OSCR

Sequential visual stimuli increase high frequency power in the visual cortex.

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 › Multi-unit and Single-unit extraction and analysis ↔ ecephys_spike_sorting/modules/quality_metrics/metrics.py, lines 20–157 · score 0.80 · quality metrics, isi violation, Isolation distance, firing rate, pre, clustering

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 · 997 lines · 35 KB · BSD-2-Clause · 1 match

  1. import numpy as np
  2. import pandas as pd
  3. from collections import OrderedDict
  4. import warnings
  5. from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
  6. from sklearn.neighbors import NearestNeighbors
  7. from sklearn.metrics import silhouette_score
  8. from scipy.spatial.distance import cdist
  9. from scipy.stats import chi2
  10. from scipy.ndimage.filters import gaussian_filter1d
  11. from ...common.epoch import Epoch
  12. from ...common.utils import printProgressBar, get_spike_depths
  13. def calculate_metrics(spike_times,
  14. spike_clusters,
  15. spike_templates,
  16. amplitudes,
  17. channel_map,
  18. pc_features,
  19. pc_feature_ind,
  20. params,
  21. epochs = None):
  22. """ Calculate metrics for all units on one probe
  23. Inputs:
  24. ------
  25. spike_times : numpy.ndarray (num_spikes x 0)
  26. Spike times in seconds (same timebase as epochs)
  27. spike_clusters : numpy.ndarray (num_spikes x 0)
  28. Cluster IDs for each spike
  29. spike_templates : numpy.ndarray (num_spikes x 0)
  30. Original template IDs for each spike time
  31. amplitudes : numpy.ndarray (num_spikes x 0)
  32. Amplitude value for each spike time
  33. channel_map : numpy.ndarray (num_units x 0)
  34. Original data channel for pc_feature_ind array
  35. pc_features : numpy.ndarray (num_spikes x num_pcs x num_channels)
  36. Pre-computed PCs for blocks of channels around each spike
  37. If 'None', PC-based metrics will not be computed
  38. pc_feature_ind : numpy.ndarray (num_units x num_channels)
  39. Channel indices of PCs for each unit
  40. epochs : list of Epoch objects
  41. contains information on Epoch start and stop times
  42. params : dict of parameters
  43. 'isi_threshold' : minimum time for isi violations
  44. Outputs:
  45. --------
  46. metrics : pandas.DataFrame
  47. one column for each metric
  48. one row per unit per epoch
  49. """
  50. metrics = pd.DataFrame()
  51. np.random.seed(9999)
  52. if epochs is None:
  53. epochs = [Epoch('complete_session', 0, np.inf)]
  54. total_units = len(np.unique(spike_clusters))
  55. total_epochs = len(epochs)
  56. for epoch in epochs:
  57. in_epoch = (spike_times > epoch.start_time) * (spike_times < epoch.end_time)
  58. print("Calculating isi violations")
  59. isi_viol = calculate_isi_violations(spike_times[in_epoch], spike_clusters[in_epoch], total_units, params['isi_threshold'], params['min_isi'])
  60. print("Calculating corrected isi violations")
  61. isi_viol_corrected = calculate_isi_violations_corrected(spike_times[in_epoch], spike_clusters[in_epoch], total_units, params['isi_threshold'], params['min_isi'])
  62. print("Calculating presence ratio")
  63. presence_ratio = calculate_presence_ratio(spike_times[in_epoch], spike_clusters[in_epoch], total_units)
  64. print("Calculating firing rate")
  65. firing_rate = calculate_firing_rate(spike_times[in_epoch], spike_clusters[in_epoch], total_units)
  66. print("Calculating amplitude cutoff")
  67. amplitude_cutoff = calculate_amplitude_cutoff(spike_clusters[in_epoch], amplitudes[in_epoch], total_units)
  68. if pc_features is not None:
  69. print("Calculating PC-based metrics")
  70. isolation_distance, l_ratio, d_prime, nn_hit_rate, nn_miss_rate = calculate_pc_metrics(spike_clusters[in_epoch],
  71. spike_templates[in_epoch],
  72. total_units,
  73. pc_features[in_epoch,:,:],
  74. pc_feature_ind,
  75. params['num_channels_to_compare'],
  76. params['max_spikes_for_unit'],
  77. params['max_spikes_for_nn'],
  78. params['n_neighbors'])
  79. print("Calculating silhouette score")
  80. the_silhouette_score = calculate_silhouette_score(spike_clusters[in_epoch],
  81. spike_templates[in_epoch],
  82. total_units,
  83. pc_features[in_epoch,:,:],
  84. pc_feature_ind,
  85. params['n_silhouette'])
  86. print("Calculating drift metrics")
  87. max_drift, cumulative_drift = calculate_drift_metrics(spike_times[in_epoch],
  88. spike_clusters[in_epoch],
  89. spike_templates[in_epoch],
  90. total_units,
  91. pc_features[in_epoch,:,:],
  92. pc_feature_ind,
  93. params['drift_metrics_interval_s'],
  94. params['drift_metrics_min_spikes_per_interval'])
  95. cluster_ids = np.unique(spike_clusters)
  96. epoch_name = [epoch.name] * len(cluster_ids)
  97. if pc_features is not None:
  98. metrics = pd.concat((metrics, pd.DataFrame(data= OrderedDict((('cluster_id', cluster_ids),
  99. ('firing_rate' , firing_rate),
  100. ('presence_ratio' , presence_ratio),
  101. ('isi_viol' , isi_viol),
  102. ('isi_viol_corrected' , isi_viol_corrected),
  103. ('amplitude_cutoff' , amplitude_cutoff),
  104. ('isolation_distance' , isolation_distance),
  105. ('l_ratio' , l_ratio),
  106. ('d_prime' , d_prime),
  107. ('nn_hit_rate' , nn_hit_rate),
  108. ('nn_miss_rate' , nn_miss_rate),
  109. ('silhouette_score', the_silhouette_score),
  110. ('max_drift', max_drift),
  111. ('cumulative_drift', cumulative_drift),
  112. ('epoch_name' , epoch_name),
  113. )))))
  114. else:
  115. metrics = pd.concat((metrics, pd.DataFrame(data= OrderedDict((('cluster_id', cluster_ids),
  116. ('firing_rate' , firing_rate),
  117. ('presence_ratio' , presence_ratio),
  118. ('isi_viol' , isi_viol),
  119. ('isi_viol_corrected' , isi_viol_corrected),
  120. ('amplitude_cutoff' , amplitude_cutoff),
  121. ('epoch_name' , epoch_name),
  122. )))))
  123. return metrics
  124. # ===============================================================
  125. # HELPER FUNCTIONS TO LOOP THROUGH CLUSTERS:
  126. # ===============================================================
  127. def calculate_isi_violations(spike_times, spike_clusters, total_units, isi_threshold, min_isi):
  128. cluster_ids = np.unique(spike_clusters)
  129. viol_rates = np.zeros((total_units,))
  130. for idx, cluster_id in enumerate(cluster_ids):
  131. printProgressBar(idx + 1, total_units)
  132. for_this_cluster = (spike_clusters == cluster_id)
  133. viol_rates[idx], num_violations = isi_violations(spike_times[for_this_cluster],
  134. min_time = np.min(spike_times),
  135. max_time = np.max(spike_times),
  136. isi_threshold=isi_threshold,
  137. min_isi = min_isi)
  138. return viol_rates
  139. def calculate_isi_violations_corrected(spike_times, spike_clusters, total_units, isi_threshold, min_isi):
  140. cluster_ids = np.unique(spike_clusters)
  141. viol_rates = np.zeros((total_units,))
  142. for idx, cluster_id in enumerate(cluster_ids):
  143. printProgressBar(idx + 1, total_units)
  144. for_this_cluster = (spike_clusters == cluster_id)
  145. viol_rates[idx], num_violations = isi_violations_corrected(spike_times[for_this_cluster],
  146. min_time = np.min(spike_times),
  147. max_time = np.max(spike_times),
  148. isi_threshold=isi_threshold,
  149. min_isi = min_isi)
  150. return viol_rates
  151. def calculate_presence_ratio(spike_times, spike_clusters, total_units):
  152. cluster_ids = np.unique(spike_clusters)
  153. ratios = np.zeros((total_units,))
  154. for idx, cluster_id in enumerate(cluster_ids):
  155. printProgressBar(idx + 1, total_units)
  156. for_this_cluster = (spike_clusters == cluster_id)
  157. ratios[idx] = presence_ratio(spike_times[for_this_cluster],
  158. min_time = np.min(spike_times),
  159. max_time = np.max(spike_times))
  160. return ratios
  161. def calculate_firing_rate(spike_times, spike_clusters, total_units):
  162. cluster_ids = np.unique(spike_clusters)
  163. firing_rates = np.zeros((total_units,))
  164. min_time = np.min(spike_times)
  165. max_time = np.max(spike_times)
  166. for idx, cluster_id in enumerate(cluster_ids):
  167. printProgressBar(idx + 1, total_units)
  168. for_this_cluster = (spike_clusters == cluster_id)
  169. firing_rates[idx] = firing_rate(spike_times[for_this_cluster],
  170. min_time = np.min(spike_times),
  171. max_time = np.max(spike_times))
  172. return firing_rates
  173. def calculate_amplitude_cutoff(spike_clusters, amplitudes, total_units):
  174. cluster_ids = np.unique(spike_clusters)
  175. amplitude_cutoffs = np.zeros((total_units,))
  176. for idx, cluster_id in enumerate(cluster_ids):
  177. printProgressBar(idx + 1, total_units)
  178. for_this_cluster = (spike_clusters == cluster_id)
  179. amplitude_cutoffs[idx] = amplitude_cutoff(amplitudes[for_this_cluster])
  180. return amplitude_cutoffs
  181. def calculate_pc_metrics_one_cluster(cluster_peak_channels, idx, cluster_id,cluster_ids,
  182. half_spread, pc_features, pc_feature_ind,
  183. spike_clusters, spike_templates,
  184. max_spikes_for_cluster, max_spikes_for_nn, n_neighbors):
  185. peak_channel = cluster_peak_channels[idx]
  186. num_spikes_in_cluster = np.sum(spike_clusters == cluster_id)
  187. half_spread_down = peak_channel \
  188. if peak_channel < half_spread \
  189. else half_spread
  190. half_spread_up = np.max(pc_feature_ind) - peak_channel \
  191. if peak_channel + half_spread > np.max(pc_feature_ind) \
  192. else half_spread
  193. channels_to_use = np.arange(peak_channel - half_spread_down, peak_channel + half_spread_up + 1)
  194. units_in_range = cluster_ids[np.isin(cluster_peak_channels, channels_to_use)]
  195. spike_counts = np.zeros(units_in_range.shape)
  196. for idx2, cluster_id2 in enumerate(units_in_range):
  197. spike_counts[idx2] = np.sum(spike_clusters == cluster_id2)
  198. if num_spikes_in_cluster > max_spikes_for_cluster:
  199. relative_counts = spike_counts / num_spikes_in_cluster * max_spikes_for_cluster
  200. else:
  201. relative_counts = spike_counts
  202. all_pcs = np.zeros((0, pc_features.shape[1], channels_to_use.size))
  203. all_labels = np.zeros((0,))
  204. for idx2, cluster_id2 in enumerate(units_in_range):
  205. subsample = int(relative_counts[idx2])
  206. pcs = get_unit_pcs(cluster_id2, spike_clusters, spike_templates,
  207. pc_feature_ind, pc_features, channels_to_use,
  208. subsample)
  209. if pcs is not None and len(pcs.shape) == 3:
  210. labels = np.ones((pcs.shape[0],)) * cluster_id2
  211. all_pcs = np.concatenate((all_pcs, pcs),0)
  212. all_labels = np.concatenate((all_labels, labels),0)
  213. all_pcs = np.reshape(all_pcs, (all_pcs.shape[0], pc_features.shape[1]*channels_to_use.size))
  214. if ((all_pcs.shape[0] > 10)
  215. and not (all_labels == cluster_id).all() # Not all labels are this cluster
  216. and (sum(all_labels == cluster_id) > 20) # No fewer than 20 spikes in this cluster
  217. and (len(channels_to_use) > 0)):
  218. isolation_distance, l_ratio = mahalanobis_metrics(all_pcs, all_labels, cluster_id)
  219. d_prime = lda_metrics(all_pcs, all_labels, cluster_id)
  220. nn_hit_rate, nn_miss_rate = nearest_neighbors_metrics(all_pcs, all_labels,
  221. cluster_id,
  222. max_spikes_for_nn,
  223. n_neighbors)
  224. else: # Too few spikes or cluster doesnt exist
  225. isolation_distance = np.nan
  226. d_prime = np.nan
  227. nn_miss_rate = np.nan
  228. nn_hit_rate = np.nan
  229. l_ratio = np.nan
  230. return isolation_distance, d_prime, nn_miss_rate, nn_hit_rate, l_ratio
  231. def calculate_pc_metrics(spike_clusters,
  232. spike_templates,
  233. total_units,
  234. pc_features,
  235. pc_feature_ind,
  236. num_channels_to_compare,
  237. max_spikes_for_cluster,
  238. max_spikes_for_nn,
  239. n_neighbors,
  240. do_parallel=True):
  241. """
  242. :param spike_clusters:
  243. :param total_units:
  244. :param pc_features:
  245. :param pc_feature_ind:
  246. :param num_channels_to_compare:
  247. :param max_spikes_for_cluster:
  248. :param max_spikes_for_nn:
  249. :param n_neighbors:
  250. :return:
  251. """
  252. assert (num_channels_to_compare % 2 == 1)
  253. half_spread = int((num_channels_to_compare - 1) / 2)
  254. cluster_ids = np.unique(spike_clusters)
  255. template_ids = np.unique(spike_templates)
  256. template_peak_channels = np.zeros((len(template_ids),), dtype='uint16')
  257. cluster_peak_channels = np.zeros((len(cluster_ids),), dtype='uint16')
  258. for idx, template_id in enumerate(template_ids):
  259. for_template = np.squeeze(spike_templates == template_id)
  260. pc_max = np.argmax(np.mean(pc_features[for_template, 0, :], 0))
  261. template_peak_channels[idx] = pc_feature_ind[template_id, pc_max]
  262. for idx, cluster_id in enumerate(cluster_ids):
  263. for_unit = np.squeeze(spike_clusters == cluster_id)
  264. templates_for_unit = np.unique(spike_templates[for_unit])
  265. template_positions = np.where(np.isin(template_ids, templates_for_unit))[0]
  266. cluster_peak_channels[idx] = np.median(template_peak_channels[template_positions])
  267. # Loop over clusters:
  268. if do_parallel:
  269. from joblib import Parallel, delayed
  270. meas = Parallel(n_jobs=-1, verbose=3)( # -1 means use all cores
  271. delayed(calculate_pc_metrics_one_cluster) # Function
  272. (cluster_peak_channels, idx, cluster_id, cluster_ids,
  273. half_spread, pc_features, pc_feature_ind,
  274. spike_clusters, spike_templates,
  275. max_spikes_for_cluster, max_spikes_for_nn, n_neighbors
  276. )
  277. for idx, cluster_id in enumerate(cluster_ids)) # Loop
  278. else:
  279. from tqdm import tqdm
  280. meas = []
  281. for idx, cluster_id in tqdm(enumerate(cluster_ids), total=cluster_ids.max(), desc='PC metrics'): # Loop
  282. meas.append(calculate_pc_metrics_one_cluster( # Function
  283. cluster_peak_channels, idx, cluster_id, cluster_ids,
  284. half_spread, pc_features, pc_feature_ind,
  285. spike_clusters, spike_templates,
  286. max_spikes_for_cluster, max_spikes_for_nn, n_neighbors))
  287. # Unpack:
  288. isolation_distances = []
  289. l_ratios = []
  290. d_primes = []
  291. nn_hit_rates = []
  292. nn_miss_rates = []
  293. for mea in meas:
  294. isolation_distance, d_prime, nn_miss_rate, nn_hit_rate, l_ratio = mea
  295. isolation_distances.append(isolation_distance)
  296. d_primes.append(d_prime)
  297. nn_miss_rates.append(nn_miss_rate)
  298. nn_hit_rates.append(nn_hit_rate)
  299. l_ratios.append(l_ratio)
  300. return (np.array(isolation_distances), np.array(l_ratios), np.array(d_primes),
  301. np.array(nn_hit_rates), np.array(nn_miss_rates))
  302. def calculate_silhouette_score(spike_clusters,
  303. spike_templates,
  304. total_units,
  305. pc_features,
  306. pc_feature_ind,
  307. total_spikes,
  308. do_parallel=True):
  309. def score_inner_loop(i, cluster_ids):
  310. """
  311. Helper to loop over cluster_ids in one dimension. We dont want to loop over both dimensions in parallel-
  312. that will create too much worker overhead
  313. Args:
  314. i: index of first dimension
  315. cluster_ids: iterable of cluster ids
  316. Returns: scores for dimension j
  317. """
  318. scores_1d = []
  319. for j in cluster_ids:
  320. if j > i:
  321. inds = np.in1d(cluster_labels, np.array([i, j]))
  322. X = all_pcs[inds, :]
  323. labels = cluster_labels[inds]
  324. # len(np.unique(labels))=1 Can happen if total_spikes is low:
  325. if (len(labels) > 2) and (len(np.unique(labels)) > 1):
  326. scores_1d.append(silhouette_score(X, labels))
  327. else:
  328. scores_1d.append(np.nan)
  329. else:
  330. scores_1d.append(np.nan)
  331. return scores_1d
  332. cluster_ids = np.unique(spike_clusters)
  333. random_spike_inds = np.random.permutation(spike_clusters.size)
  334. random_spike_inds = random_spike_inds[:total_spikes]
  335. num_pc_features = pc_features.shape[1]
  336. num_channels = np.max(pc_feature_ind) + 1
  337. all_pcs = np.zeros((total_spikes, num_channels * num_pc_features))
  338. for idx, i in enumerate(random_spike_inds):
  339. unit_id = spike_templates[i]
  340. channels = pc_feature_ind[unit_id,:]
  341. for j in range(0,num_pc_features):
  342. all_pcs[idx, channels + num_channels * j] = pc_features[i,j,:]
  343. cluster_labels = np.squeeze(spike_clusters[random_spike_inds])
  344. SS = np.empty((total_units, total_units))
  345. SS[:] = np.nan
  346. # Build lists
  347. if do_parallel:
  348. from joblib import Parallel, delayed
  349. scores = Parallel(n_jobs=-1, verbose=2)(delayed(score_inner_loop)(i, cluster_ids) for i in cluster_ids)
  350. else:
  351. scores = [score_inner_loop(i, cluster_ids) for i in cluster_ids]
  352. # Fill the 2d array
  353. for i, col_score in enumerate(scores):
  354. for j, one_score in enumerate(col_score):
  355. SS[i, j] = one_score
  356. with warnings.catch_warnings():
  357. warnings.simplefilter("ignore")
  358. a = np.nanmin(SS, 0)
  359. b = np.nanmin(SS, 1)
  360. return np.array([np.nanmin([a,b]) for a, b in zip(a,b)])
  361. def calculate_drift_metrics(spike_times,
  362. spike_clusters,
  363. spike_templates,
  364. total_units,
  365. pc_features,
  366. pc_feature_ind,
  367. interval_length,
  368. min_spikes_per_interval,
  369. do_parallel=True):
  370. def calc_one_cluster(cluster_id):
  371. """
  372. Helper to calculate drift for one cluster
  373. Args:
  374. cluster_id:
  375. Returns:
  376. max_drift, cumulative_drift
  377. """
  378. in_cluster = spike_clusters == cluster_id
  379. times_for_cluster = spike_times[in_cluster]
  380. depths_for_cluster = depths[in_cluster]
  381. median_depths = []
  382. for t1, t2 in zip(interval_starts, interval_ends):
  383. in_range = (times_for_cluster > t1) * (times_for_cluster < t2)
  384. if np.sum(in_range) >= min_spikes_per_interval:
  385. median_depths.append(np.median(depths_for_cluster[in_range]))
  386. else:
  387. median_depths.append(np.nan)
  388. median_depths = np.array(median_depths)
  389. max_drift = np.around(np.nanmax(median_depths) - np.nanmin(median_depths), 2)
  390. cumulative_drift = np.around(np.nansum(np.abs(np.diff(median_depths))), 2)
  391. return max_drift, cumulative_drift
  392. max_drifts = []
  393. cumulative_drifts = []
  394. depths = get_spike_depths(spike_templates, pc_features, pc_feature_ind)
  395. interval_starts = np.arange(np.min(spike_times), np.max(spike_times), interval_length)
  396. interval_ends = interval_starts + interval_length
  397. cluster_ids = np.unique(spike_clusters)
  398. if do_parallel:
  399. from joblib import Parallel, delayed
  400. meas = Parallel(n_jobs=-1, verbose=2)(delayed(calc_one_cluster)(cluster_id)
  401. for cluster_id in cluster_ids)
  402. else:
  403. meas = [calc_one_cluster(cluster_id) for cluster_id in cluster_ids]
  404. for max_drift, cumulative_drift in meas:
  405. max_drifts.append(max_drift)
  406. cumulative_drifts.append(cumulative_drift)
  407. return np.array(max_drifts), np.array(cumulative_drifts)
  408. # ==========================================================
  409. # IMPLEMENTATION OF ACTUAL METRICS:
  410. # ==========================================================
  411. def isi_violations(spike_train, min_time, max_time, isi_threshold, min_isi=0):
  412. """Calculate ISI violations for a spike train.
  413. Based on metric described in Hill et al. (2011) J Neurosci 31: 8699-8705
  414. modified by Dan Denman from cortex-lab/sortingQuality GitHub by Nick Steinmetz
  415. Inputs:
  416. -------
  417. spike_train : array of spike times
  418. min_time : minimum time for potential spikes
  419. max_time : maximum time for potential spikes
  420. isi_threshold : threshold for isi violation
  421. min_isi : threshold for duplicate spikes
  422. Outputs:
  423. --------
  424. fpRate : rate of contaminating spikes as a fraction of overall rate
  425. A perfect unit has a fpRate = 0
  426. A unit with some contamination has a fpRate < 0.5
  427. A unit with lots of contamination has a fpRate > 1.0
  428. num_violations : total number of violations
  429. """
  430. duplicate_spikes = np.where(np.diff(spike_train) <= min_isi)[0]
  431. spike_train = np.delete(spike_train, duplicate_spikes + 1)
  432. isis = np.diff(spike_train)
  433. num_spikes = len(spike_train)
  434. num_violations = sum(isis < isi_threshold)
  435. violation_time = 2*num_spikes*(isi_threshold - min_isi)
  436. total_rate = firing_rate(spike_train, min_time, max_time)
  437. violation_rate = num_violations/violation_time
  438. fpRate = violation_rate/total_rate
  439. return fpRate, num_violations
  440. def isi_violations_corrected(spike_train, min_time, max_time, isi_threshold, min_isi=0):
  441. """Calculate ISI violations for a spike train (with bias correction).
  442. This function was updated in September 2023 by Nick Steinmetz to correct
  443. two problems with the original implementation (text copied from
  444. https://github.com/cortex-lab/sortingQuality repo):
  445. 1) The approximation previously used, which was chosen to avoid getting
  446. imaginary results, wasn't accurate to the Hill et al paper on which this
  447. method was based, nor was it accurate to the correct solution to the problem.
  448. 2) Hill et al also did not have the correct solution to the problem. The Hill
  449. paper used an expression derived from an earlier work (Meunier et al
  450. 2003) which had assumed a special case: the "contamination" was itself
  451. only generated by a single neuron and therefore the contaminating spikes
  452. themselves had a refractory period. If instead the contaminating spikes
  453. are generated from a real Poisson process (as in the case of electrical
  454. noise or many nearby neurons generating the contamination), then the
  455. correct expression is different, as now calculated here. This expression
  456. is given in Llobet et al. bioRxiv 2022:
  457. https://www.biorxiv.org/content/10.1101/2022.02.08.479192v1.full.pdf
  458. In practice, the three methods (the real Hill equation, the original
  459. isi_violations calculation, and the correct equation implemented below)
  460. return almost identical values for contamination less than ~20%. They
  461. diverge strongly for 30% or more. For contamination levels above 50%
  462. (based on the original calculation), the corrected version is undefined
  463. due to a negative square root.
  464. Inputs:
  465. -------
  466. spike_train : array of spike times
  467. min_time : minimum time for potential spikes
  468. max_time : maximum time for potential spikes
  469. isi_threshold : threshold for isi violation
  470. min_isi : threshold for duplicate spikes
  471. Outputs:
  472. --------
  473. fpRate : rate of contaminating spikes as a fraction of overall rate
  474. A perfect unit has a fpRate = 0
  475. A unit with some contamination has a fpRate < 0.5
  476. A unit with lots of contamination has a fpRate > 1.0
  477. num_violations : total number of violations
  478. """
  479. duplicate_spikes = np.where(np.diff(spike_train) <= min_isi)[0]
  480. spike_train = np.delete(spike_train, duplicate_spikes + 1)
  481. isis = np.diff(spike_train)
  482. duration = max_time - min_time
  483. num_spikes = len(spike_train)
  484. num_violations = sum(isis < isi_threshold)
  485. fpRate = 1 - np.sqrt(1 - num_violations * duration /
  486. (pow(num_spikes, 2) * (isi_threshold - min_isi)))
  487. return fpRate, num_violations
  488. def presence_ratio(spike_train, min_time, max_time, num_bins=100):
  489. """Calculate fraction of time the unit is present within an epoch.
  490. Inputs:
  491. -------
  492. spike_train : array of spike times
  493. min_time : minimum time for potential spikes
  494. max_time : maximum time for potential spikes
  495. Outputs:
  496. --------
  497. presence_ratio : fraction of time bins in which this unit is spiking
  498. """
  499. h, b = np.histogram(spike_train, np.linspace(min_time, max_time, num_bins))
  500. return np.sum(h > 0) / (num_bins - 1)
  501. def firing_rate(spike_train, min_time = None, max_time = None):
  502. """Calculate firing rate for a spike train.
  503. If no temporal bounds are specified, the first and last spike time are used.
  504. Inputs:
  505. -------
  506. spike_train : numpy.ndarray
  507. Array of spike times in seconds
  508. min_time : float
  509. Time of first possible spike (optional)
  510. max_time : float
  511. Time of last possible spike (optional)
  512. Outputs:
  513. --------
  514. fr : float
  515. Firing rate in Hz
  516. """
  517. if min_time is not None and max_time is not None:
  518. duration = max_time - min_time
  519. else:
  520. duration = np.max(spike_train) - np.min(spike_train)
  521. fr = spike_train.size / duration
  522. return fr
  523. def amplitude_cutoff(amplitudes, num_histogram_bins = 500, histogram_smoothing_value = 3):
  524. """ Calculate approximate fraction of spikes missing from a distribution of amplitudes
  525. Assumes the amplitude histogram is symmetric (not valid in the presence of drift)
  526. Inspired by metric described in Hill et al. (2011) J Neurosci 31: 8699-8705
  527. Input:
  528. ------
  529. amplitudes : numpy.ndarray
  530. Array of amplitudes (don't need to be in physical units)
  531. Output:
  532. -------
  533. fraction_missing : float
  534. Fraction of missing spikes (0-0.5)
  535. If more than 50% of spikes are missing, an accurate estimate isn't possible
  536. """
  537. h,b = np.histogram(amplitudes, num_histogram_bins, density=True)
  538. pdf = gaussian_filter1d(h,histogram_smoothing_value)
  539. support = b[:-1]
  540. peak_index = np.argmax(pdf)
  541. G = np.argmin(np.abs(pdf[peak_index:] - pdf[0])) + peak_index
  542. bin_size = np.mean(np.diff(support))
  543. fraction_missing = np.sum(pdf[G:])*bin_size
  544. fraction_missing = np.min([fraction_missing, 0.5])
  545. return fraction_missing
  546. def mahalanobis_metrics(all_pcs, all_labels, this_unit_id):
  547. """ Calculates isolation distance and L-ratio (metrics computed from Mahalanobis distance)
  548. Based on metrics described in Schmitzer-Torbert et al. (2005) Neurosci 131: 1-11
  549. Inputs:
  550. -------
  551. all_pcs : numpy.ndarray (num_spikes x PCs)
  552. 2D array of PCs for all spikes
  553. all_labels : numpy.ndarray (num_spikes x 0)
  554. 1D array of cluster labels for all spikes
  555. this_unit_id : Int
  556. number corresponding to unit for which these metrics will be calculated
  557. Outputs:
  558. --------
  559. isolation_distance : float
  560. Isolation distance of this unit
  561. l_ratio : float
  562. L-ratio for this unit
  563. """
  564. pcs_for_this_unit = all_pcs[all_labels == this_unit_id,:]
  565. pcs_for_other_units = all_pcs[all_labels != this_unit_id, :]
  566. mean_value = np.expand_dims(np.mean(pcs_for_this_unit,0),0)
  567. try:
  568. VI = np.linalg.inv(np.cov(pcs_for_this_unit.T))
  569. except np.linalg.linalg.LinAlgError: # case of singular matrix
  570. return np.nan, np.nan
  571. mahalanobis_other = np.sort(cdist(mean_value,
  572. pcs_for_other_units,
  573. 'mahalanobis', VI = VI)[0])
  574. mahalanobis_self = np.sort(cdist(mean_value,
  575. pcs_for_this_unit,
  576. 'mahalanobis', VI = VI)[0])
  577. n = np.min([pcs_for_this_unit.shape[0], pcs_for_other_units.shape[0]]) # number of spikes
  578. if n >= 2:
  579. dof = pcs_for_this_unit.shape[1] # number of features
  580. l_ratio = np.sum(1 - chi2.cdf(pow(mahalanobis_other,2), dof)) / \
  581. mahalanobis_self.shape[0] # normalize by size of cluster, not number of other spikes
  582. isolation_distance = pow(mahalanobis_other[n-1],2)
  583. else:
  584. l_ratio = np.nan
  585. isolation_distance = np.nan
  586. return isolation_distance, l_ratio
  587. def lda_metrics(all_pcs, all_labels, this_unit_id):
  588. """ Calculates d-prime based on Linear Discriminant Analysis
  589. Based on metric described in Hill et al. (2011) J Neurosci 31: 8699-8705
  590. Inputs:
  591. -------
  592. all_pcs : numpy.ndarray (num_spikes x PCs)
  593. 2D array of PCs for all spikes
  594. all_labels : numpy.ndarray (num_spikes x 0)
  595. 1D array of cluster labels for all spikes
  596. this_unit_id : Int
  597. number corresponding to unit for which these metrics will be calculated
  598. Outputs:
  599. --------
  600. d_prime : float
  601. Isolation distance of this unit
  602. l_ratio : float
  603. L-ratio for this unit
  604. """
  605. X = all_pcs
  606. y = np.zeros((X.shape[0],),dtype='bool')
  607. y[all_labels == this_unit_id] = True
  608. lda = LDA(n_components=1)
  609. X_flda = lda.fit_transform(X, y)
  610. flda_this_cluster = X_flda[np.where(y)[0]]
  611. flda_other_cluster = X_flda[np.where(np.invert(y))[0]]
  612. d_prime = (np.mean(flda_this_cluster) - np.mean(flda_other_cluster))/np.sqrt(0.5*(np.std(flda_this_cluster)**2+np.std(flda_other_cluster)**2))
  613. return d_prime
  614. def nearest_neighbors_metrics(all_pcs, all_labels, this_unit_id, max_spikes_for_nn, n_neighbors):
  615. """ Calculates unit contamination based on NearestNeighbors search in PCA space
  616. Based on metrics described in Chung, Magland et al. (2017) Neuron 95: 1381-1394
  617. Inputs:
  618. -------
  619. all_pcs : numpy.ndarray (num_spikes x PCs)
  620. 2D array of PCs for all spikes
  621. all_labels : numpy.ndarray (num_spikes x 0)
  622. 1D array of cluster labels for all spikes
  623. this_unit_id : Int
  624. number corresponding to unit for which these metrics will be calculated
  625. max_spikes_for_nn : Int
  626. number of spikes to use (calculation can be very slow when this number is >20000)
  627. n_neighbors : Int
  628. number of neighbors to use
  629. Outputs:
  630. --------
  631. hit_rate : float
  632. Fraction of neighbors for target cluster that are also in target cluster
  633. miss_rate : float
  634. Fraction of neighbors outside target cluster that are in target cluster
  635. """
  636. total_spikes = all_pcs.shape[0]
  637. ratio = max_spikes_for_nn / total_spikes
  638. this_unit = all_labels == this_unit_id
  639. X = np.concatenate((all_pcs[this_unit,:], all_pcs[np.invert(this_unit),:]),0)
  640. n = np.sum(this_unit)
  641. if ratio < 1:
  642. inds = np.arange(0,X.shape[0]-1,1/ratio).astype('int')
  643. X = X[inds,:]
  644. n = int(n * ratio)
  645. nbrs = NearestNeighbors(n_neighbors=n_neighbors, algorithm='ball_tree').fit(X)
  646. distances, indices = nbrs.kneighbors(X)
  647. this_cluster_inds = np.arange(n)
  648. this_cluster_nearest = indices[:n,1:].flatten()
  649. other_cluster_nearest = indices[n:,1:].flatten()
  650. hit_rate = np.mean(this_cluster_nearest < n)
  651. miss_rate = np.mean(other_cluster_nearest < n)
  652. return hit_rate, miss_rate
  653. # ==========================================================
  654. # HELPER FUNCTIONS:
  655. # ==========================================================
  656. def features_intersect(pc_feature_ind, these_channels):
  657. """
  658. # Take only the channels that have calculated features out of the ones we are interested in:
  659. # This should reduce the occurence of 'except IndexError' below
  660. Args:
  661. pc_feature_ind
  662. these_channels: channels_to_use or units_for_channel
  663. Returns:
  664. channels_to_use: intersect of what's available in PCs and what's needed
  665. """
  666. intersect = set(pc_feature_ind[these_channels[0], :]) # Initialize
  667. for cluster_id2 in these_channels:
  668. # Make a running intersect of what is available and what is needed
  669. intersect = intersect & set(pc_feature_ind[cluster_id2, :])
  670. return np.array(list(intersect))
  671. def get_unit_pcs(unit_id,
  672. spike_clusters,
  673. spike_templates,
  674. pc_feature_ind,
  675. pc_features,
  676. channels_to_use,
  677. subsample):
  678. """ Return PC features for one unit
  679. Inputs:
  680. -------
  681. unit_id : Int
  682. ID for this unit
  683. spike_clusters : np.ndarray
  684. Cluster labels for each spike
  685. spike_templates : np.ndarry
  686. Template labels for each spike
  687. pc_feature_ind : np.ndarray
  688. Channels used for PC calculation for each unit
  689. pc_features : np.ndarray
  690. Array of all PC features
  691. channels_to_use : np.ndarray
  692. Channels to use for calculating metrics
  693. subsample : Int
  694. maximum number of spikes to return
  695. Output:
  696. -------
  697. unit_PCs : numpy.ndarray (float)
  698. PCs for one unit (num_spikes x num_PCs x num_channels)
  699. """
  700. inds_for_unit = np.where(spike_clusters == unit_id)[0]
  701. spikes_to_use = np.random.permutation(inds_for_unit)[:subsample]
  702. unique_template_ids = np.unique(spike_templates[spikes_to_use])
  703. unit_PCs = []
  704. for template_id in unique_template_ids:
  705. index_mask = spikes_to_use[np.squeeze(spike_templates[spikes_to_use]) == template_id]
  706. these_inds = pc_feature_ind[template_id, :]
  707. pc_array = []
  708. for i in channels_to_use:
  709. if np.isin(i, these_inds):
  710. channel_index = np.argwhere(these_inds == i)[0][0]
  711. pc_array.append(pc_features[index_mask, :, channel_index])
  712. else:
  713. return None
  714. unit_PCs.append(np.stack(pc_array, axis=-1))
  715. if len(unit_PCs) > 0:
  716. return np.concatenate(unit_PCs)
  717. else:
  718. return None

metrics.py at commit 9199927, under BSD-2-Clause · at the source

Overview

Authors: Julian Keil1,2,3, Victor Hernandez-Urbina2, Chrystalleni Vassiliou4,5, Camin Dean4,5,6,7, Dietmar Schmitz4,5,6,7,8,9, Jens Kremkow7,10, Jérémie Sibille4,5,6,7,8,9
ORCID iDs: Dietmar Schmitz
  1. Department of Cognitive Science, University of Potsdam, 14469 Potsdam, Germany
  2. Nuuron GmbH Berlin, Berlin, Germany
  3. Charité-Universitätsmedizin Berlin, Corporate Member of Freie Universität Berlin and Humboldt-Universität Berlin, Clinical Neurotechnology Laboratory, Department of Psychiatry and Neurosciences, 10117 Berlin, Germany
  4. German Center for Neurodegenerative Diseases (DZNE) Berlin, 10117 Berlin, Germany
  5. Charité-Universitätsmedizin Berlin, Corporate Member of Freie Universität Berlin and Humboldt-Universität Berlin, Neuroscience Research Center, 10117 Berlin, Germany
  6. Charité-Universitätsmedizin Berlin, Corporate Member of Freie Universität Berlin and Humboldt-Universität Berlin, Einstein Center for Neurosciences, 10117 Berlin, Germany
  7. Charité-Universitätsmedizin Berlin, Corporate Member of Freie Universität Berlin and Humboldt-Universität Berlin, Bernstein Center for Computational Neuroscience, 10115 Berlin, Germany
  8. Charité-Universitätsmedizin Berlin, Corporate Member of Freie Universität Berlin and Humboldt-Universität Berlin, NeuroCure Cluster of Excellence, 10117 Berlin, Germany
  9. Charité-Universitätsmedizin Berlin, Corporate Member of Freie Universität Berlin and Humboldt-Universität zu Berlin, Institute of Cell and Neurobiology, 10115 Berlin, Germany
  10. Institute of Biology, Otto von Guericke University Magdeburg, 39120 Magdeburg, Germany
Journal: Scientific reports, volume 16, issue 1, article 15228
Dates: received 26 January 2026; accepted 4 May 2026; published online 17 May 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41598-026-52253-9 · PMID 42144416 · PMCID PMC13181131 · OpenAlex W4406545532
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: mouse (organism), systems (subfield)
Methods: Spectral & time-frequency, Statistics, Preprocessing, Evoked potentials, Single-unit activity, calcium imaging
Keywords: Biological techniques, Neuroscience
MeSH: Photic Stimulation*, Visual Cortex*, Animals, Mice (* major topic)
Topic: Neural dynamics and brain function (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Charité - Universitätsmedizin Berlin
Citations: cited by 1 paper (Europe PMC); 43 references in the paper

Abstract

Today, flickering full-field visual stimulation is used to increase neuronal oscillations for a variety of research or therapeutic purposes. We propose spatially organized sequential visual flickering stimulation as a newer tool to circumvent the intrinsic low-pass filter nature of the vertebrate visual system, in order to increase the power of high frequency oscillations in the visual system. We show that spatially organized visual flickering can increase power in high frequencies (100 to 190 Hz) in the visual cortex of mice. Consequently, spatially organized sequential sensory stimulation should be regarded as a putative new way leading to power increases in high frequency domains.

Supplementary Information: The online version contains supplementary material available at 10.1038/s41598-026-52253-9.

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.

AllenInstitute/ecephys_spike_sorting

License: BSD-2-Clause
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 919992748a5324724ba87169ecdcf9eb6e3b9973, 18 August 2024
Languages: Python (77), C++ (19), C/C++ (10), JavaScript (5), Shell (2), MATLAB (1)
Size: 216 files, 114 scripts
Software Heritage: not archived
Found in: the text, “Hardware, software”
Holds: README, license file, environment (Pipfile, Pipfile.lock, setup.cfg, setup.py), tests, continuous integration, documentation
Not found: CITATION.cff
Tools: NumPy (34 files), pandas (11 files), SciPy (9 files), Matplotlib (8 files), scikit-learn (4 files), xarray (2 files), h5py (1 file), Kilosort (1 file), Pillow (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
116 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;
  • 114 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

Data and scripts will be provided freely upon reasonable request.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 7 authors, 2 keywords, 4 MeSH terms, 1 funder, 36 references.

Cite

This paper

Keil, J., Hernandez-Urbina, V., Vassiliou, C., Dean, C., Schmitz, D., Kremkow, J., & Sibille, J. (2026). Sequential visual stimuli increase high frequency power in the visual cortex. Scientific reports, 16(1), 15228. https://doi.org/10.1038/s41598-026-52253-9

BibTeX

@article{keil2026sequential,
author = {Keil, Julian and Hernandez-Urbina, Victor and Vassiliou, Chrystalleni and Dean, Camin and Schmitz, Dietmar and Kremkow, Jens and Sibille, Jérémie},
title = {{Sequential visual stimuli increase high frequency power in the visual cortex}},
journal = {Scientific reports},
year = {2026},
month = may,
volume = {16},
number = {1},
pages = {15228},
publisher = {Nature Publishing Group},
issn = {2045-2322},
doi = {10.1038/s41598-026-52253-9},
url = {https://doi.org/10.1038/s41598-026-52253-9},
pmid = {42144416},
pmcid = {PMC13181131}
}

RIS

TY - JOUR
AU - Keil, Julian
AU - Hernandez-Urbina, Victor
AU - Vassiliou, Chrystalleni
AU - Dean, Camin
AU - Schmitz, Dietmar
AU - Kremkow, Jens
AU - Sibille, Jérémie
TI - Sequential visual stimuli increase high frequency power in the visual cortex
T2 - Scientific reports
J2 - Sci Rep
PY - 2026
DA - 2026/05/17
VL - 16
IS - 1
SP - 15228
SN - 2045-2322
PB - Nature Publishing Group
DO - 10.1038/s41598-026-52253-9
UR - https://doi.org/10.1038/s41598-026-52253-9
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41598-026-52253-9",
"type": "article-journal",
"title": "Sequential visual stimuli increase high frequency power in the visual cortex",
"container-title": "Scientific reports",
"author": [
{
"family": "Keil",
"given": "Julian"
},
{
"family": "Hernandez-Urbina",
"given": "Victor"
},
{
"family": "Vassiliou",
"given": "Chrystalleni"
},
{
"family": "Dean",
"given": "Camin"
},
{
"family": "Schmitz",
"given": "Dietmar"
},
{
"family": "Kremkow",
"given": "Jens"
},
{
"family": "Sibille",
"given": "Jérémie"
}
],
"container-title-short": "Sci Rep",
"volume": "16",
"issue": "1",
"page": "15228",
"DOI": "10.1038/s41598-026-52253-9",
"PMID": "42144416",
"PMCID": "PMC13181131",
"ISSN": "2045-2322",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41598-026-52253-9",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
17
]
]
}
}

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-02232-0 [code]
Entorhinal cortex represents task-relevant remote locations independently of CA1.
Journal: Nature neuroscience
In common: Kilosort, xarray, h5py, 6 other tools, systems, mouse, 1 reference
[2] doi:10.1162/imag.a.1229 [code]
40 Hz audiovisual stimulation improves sustained attention and related brain oscillations.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: pandas, SciPy, Matplotlib, 1 other tool, 7 references
[3] doi:10.1038/s41467-026-76581-6 [code]
Thalamocortical bursts encode reward contingencies and drive associative learning.
Journal: Nature communications
In common: Kilosort, xarray, h5py, 6 other tools, systems, mouse
[4] doi:10.7554/elife.110588 [code]
Opening the black box toward a modular approach to spike sorting.
Journal: eLife
In common: Kilosort, h5py, Pillow, 5 other tools, mouse, 2 references
[5] doi:10.1038/s41593-026-02258-4 [code]
Laminar organization of cellular microcircuits modulating human interictal epileptiform discharges.
Journal: Nature neuroscience
In common: Kilosort, scikit-learn, pandas, 3 other tools, systems, 3 references
[6] 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: Kilosort, h5py, Pillow, 5 other tools, 1 reference
[7] doi:10.7554/elife.109717 [code]
Retrosplenial cortex enables context-dependent goal-directed sensorimotor transformation.
Journal: eLife
In common: Kilosort, h5py, Pillow, 5 other tools, systems, mouse
[8] doi:10.1038/s41467-026-72057-9 [code]
Sex-specific behavioral feedback modulates sensorimotor processing and drives flexible social behavior.
Journal: Nature communications
In common: xarray, h5py, Pillow, 5 other tools, systems
[9] doi:10.1038/s41586-026-10331-y [code]
Active dissociation of intracortical spiking and high gamma activity.
Journal: Nature
In common: h5py, Pillow, scikit-learn, 4 other tools, 2 references
[10] doi:10.1098/rstb.2024.0461 [code]
Shallow recurrent decoders for neural and behavioural dynamics.
Journal: Philosophical transactions of the Royal Society of London. Series B, Biological sciences
In common: xarray, h5py, Pillow, 5 other tools, mouse

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.