OSCR

A flexible quality metric for electrophysiological recordings across brain regions and species

Code ↔ Paper

16 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 16 matches · 5 of them tie a paragraph to a whole file, not to given lines: weak matches, whose lines are not tinted
  1. [1] § Methods › False acceptance rate in the Sliding RP metric ↔ matlab/computeMatrix.m, lines 2–59 · score 0.79 · Markov chain, error rate, expected violations, passage, family, Sliding RP
  2. [2] § Methods › False acceptance rate in the Sliding RP metric ↔ python/slidingRP/metrics.py, lines 468–522 · score 0.76 · Markov chain, passage probability, error rate, family, Sliding RP, Poisson
  3. [3] § Methods › Estimating RP duration for comparison across regions and species (Fig. 1) ↔ roth-et-al-2026/rpQuantification/estimate_refractory_period.m, lines 45–55 · score 0.70 · peak detection, 0–0.5, narrow, outside, refractory period, filtered
  4. [4] § Methods › Algorithm Description › Step 4: Computing Confidence Scores ↔ matlab/computeViol.m, the whole file · a weak match · score 0.69 · fewer violations, violations Ve, RP violations, cumulative, Scores, probability
  5. [5] § Methods › Estimating RP duration for comparison across regions and species (Fig. 1) ↔ python/slidingRP/metrics.py, lines 34–126 · score 0.66 · 0–0.5, filtered ACG, global, outside, peak, milliseconds
  6. [6] § Methods › Algorithm Description › Step 2: Computing Expected Violations ↔ matlab/computeViol.m, the whole file · a weak match · score 0.65 · neuron spikes, contamination proportion, RP violations, spike train, hypothesis, contaminating spikes
  7. [7] § Methods › Estimating RP duration for comparison across regions and species (Fig. 1) ↔ roth-et-al-2026/rpQuantification/estimate_refractory_period.m, lines 57–79 · score 0.63 · valid peaks, refractory period, earliest, closest, filtered, ACG
  8. [8] § Results › A quality metric suitable for variable RP durations ↔ matlab/slidingRP_all.m, the whole file · a weak match · score 0.63 · maximum acceptable contamination, maximum confidence, Sliding RP metric, lowest, variable, matrix
  9. [9] § Methods › Estimating RP duration for comparison across regions and species (Fig. 1) ↔ roth-et-al-2026/rpQuantification/estimate_refractory_period.m, lines 92–136 · score 0.60 · fit parameters, midpoint, steepness, refractory period, ymax, ymin
  10. [10] § Results › Influence of Confidence parameter ↔ matlab/RPmetric_Classic.m, the whole file · a weak match · score 0.59 · statistical power, Hill Llobet, contamination threshold, matching, firing rate, Sliding RP
  11. [11] § Methods › Estimating RP duration for comparison across regions and species (Fig. 1) ↔ python/slidingRP/metrics.py, lines 34–126 · score 0.58 · filtered ACG, aborted, strictly, closest, baseline, peaks
  12. [12] § Results › A quality metric suitable for variable RP durations ↔ matlab/slidingRP.m, lines 2–68 · score 0.58 · maximum acceptable contamination, maximum confidence, Sliding RP, lowest, matrix, quality
  13. [13] § Methods › Algorithm Description › Step 1: Autocorrelogram Computation ↔ python/slidingRP/metrics.py, lines 361–398 · score 0.57 · spike pairs, nACG, histogram, lag, autocorrelogram, bin
  14. [14] § Results › Validation of the metric with simulated data ↔ matlab/slidingRP.m, lines 2–68 · score 0.56 · low firing rates, maximum acceptable, S3, spike trains, conservatively, Sliding RP
  15. [15] § Methods › Spike train simulations ↔ matlab/simulations/genST.m, the whole file · a weak match · score 0.55 · cumulative sum, exprnd, exponentially, spike train, vector, recording duration
  16. [16] § Methods › Estimating RP duration for comparison across regions and species (Fig. 1) ↔ roth-et-al-2026/rpQuantification/estimate_refractory_period.m, lines 36–43 · score 0.52 · median filter, smoothed, milliseconds, window, bin, ACG

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 · 776 lines · 32 KB · no license · 4 matches

  1. # -*- coding: utf-8 -*-
  2. """
  3. Created on Sun Jul 10 11:34:59 2022
  4. @author: Gaelle Chapuis ; Legacy code from Noam Roth commented below
  5. compute the metric for a single cluster (neuron) in a recording
  6. """
  7. import warnings
  8. from scipy.optimize import OptimizeWarning
  9. import numpy as np
  10. from scipy import stats
  11. from scipy.optimize import curve_fit
  12. import scipy
  13. def closest(lst, K):
  14. lst = np.asarray(lst)
  15. idx = (np.abs(lst - K)).argmin()
  16. return idx, lst[idx]
  17. def compute_timebins(acg, bin_size_secs):
  18. x = np.arange(len(acg))
  19. timeBins = x.dot(bin_size_secs)
  20. return timeBins
  21. def sigmoid(x, L, x0, k, b): # L is max value; x0 is the midpoint x value; k is the steepness, b is the baseline shift
  22. y = L / (1 + np.exp(-k * (x - x0))) + b
  23. return y
  24. def compute_rf(acg,
  25. min_sig=np.array([0.0, 0.0005]), # 0-0.5 ms
  26. t_medfilter=0.00083,
  27. bin_size_secs=1 / 30_000,
  28. timeBins=None,
  29. RPEstimateFromPercentageOfSlope = 0.05,
  30. fr_percentage=10 / 100):
  31. '''
  32. Compute the refractory period (RP) from an auto-correlogram (ACG) array, by :
  33. - filtering the ACG (median filter)
  34. - finding the ACG first local peak above a certain firing rate threshold
  35. - fitting a sigmoid to the first portion of the filtered ACG
  36. - The RP is defined as the time at which a certain percentage of the sigmoid fit is reached.
  37. :param acg: the autocorrelogram, 1D numpy array containing spike number in bins of size bin_size_secs
  38. :param min_sig: time window in second to compute the minimum of the sigmoid fit
  39. :param t_medfilter: window for the median filter, in seconds
  40. :param bin_size_secs: size of the acg bins, in seconds
  41. :param timeBins: time of the bins, re-computed if None
  42. :param RPEstimateFromPercentageOfSlope: portion of the sigmoid fit at which to take the time as RP; value from 0-1
  43. :param fr_percentage: percentage of the max-min firing rate, used to find the first peak.
  44. :return:
  45. estimatedRP: the estimated RP in milliseconds
  46. estimateIdx: the index of the RP (which corresponds to the bin index of the ACG)
  47. xSigmoid, ySigmoid: the sigmoid fit (x-values: times, y-values: curve values)
  48. '''
  49. # This is a hack: we do not want to return the fit on an ACG where the fit is poor
  50. # Treat a poor curve_fit as a failure, but only within this function
  51. # (catch_warnings restores the global filter state on exit).
  52. with warnings.catch_warnings():
  53. warnings.simplefilter("error", OptimizeWarning)
  54. if timeBins is None: # Compute timebins
  55. timeBins = compute_timebins(acg, bin_size_secs)
  56. # Median filter
  57. med_filt = scipy.ndimage.median_filter(acg, size=int(np.round(t_medfilter / bin_size_secs)))
  58. # Find all the peaks on this filtered trace
  59. peaks_idx = scipy.signal.find_peaks(med_filt)[0]
  60. # Compute value (max) in short time window near 0
  61. minSigmoid = np.max(med_filt[(timeBins > min_sig[0]) & (timeBins < min_sig[1])])
  62. # Note: could be using either the max() or mean()
  63. # max forces the peak to be outside of this time window
  64. # To find the peak, make sure it is strictly higher than this value
  65. # and above a baseline percentage firing rate
  66. fr_baseline = fr_percentage * (med_filt.max() - med_filt.min())
  67. peaks_possible = np.where((med_filt[peaks_idx] > minSigmoid) &
  68. (med_filt[peaks_idx] > fr_baseline))[0]
  69. # if no peak is found, abort and return NaN
  70. if len(peaks_possible) == 0:
  71. estimatedRP = np.nan
  72. estimateIdx = np.nan
  73. xSigmoid = np.array([])
  74. ySigmoid = np.array([])
  75. return estimatedRP, estimateIdx, xSigmoid, ySigmoid
  76. # Use first peak possible found (i.e. closest to 0 second on the ACG)
  77. peak_idx = peaks_idx[peaks_possible[0]]
  78. maxSigmoid = med_filt[peak_idx]
  79. # Truncate ACG and time bins according to max
  80. timeBins_fit = timeBins[0:peak_idx]
  81. acg_fit = med_filt[0:peak_idx]
  82. # fit the sigmoid with max and min fixed
  83. try:
  84. popt, pcov = curve_fit(lambda x, x0, k: sigmoid(x, maxSigmoid, x0, k, minSigmoid), timeBins_fit, acg_fit)
  85. fitParams = [maxSigmoid, popt[0], popt[1], minSigmoid]
  86. xSigmoid = timeBins
  87. ySigmoid = sigmoid(xSigmoid, *fitParams)
  88. # find RP
  89. estimateIdx, _ = closest(ySigmoid, RPEstimateFromPercentageOfSlope * (maxSigmoid - minSigmoid) + minSigmoid)
  90. # Compute the index of the first ACG bin with non-null firing rate
  91. first_index = np.where(acg != 0)[0][0]
  92. if estimateIdx < first_index: # If the estimate RP is BEFORE the first ACG bin
  93. estimateIdx = first_index # Replace
  94. estimatedRP = 1000 * xSigmoid[estimateIdx] # in ms
  95. if np.max(ySigmoid) - np.min(ySigmoid) < 1: # The fit is essentially flat
  96. raise OptimizeWarning
  97. except (OptimizeWarning, RuntimeWarning, RuntimeError): # This is in the case the bins are too few
  98. # print('fit error')
  99. estimatedRP = np.nan
  100. estimateIdx = np.nan
  101. xSigmoid = np.array([])
  102. ySigmoid = np.array([])
  103. return estimatedRP, estimateIdx, xSigmoid, ySigmoid
  104. def remove_lowrp_confmat(confMatrix, rp, rp_reject=0.0005):
  105. # We want to compute on the matrix only for RPs above a certain value
  106. # Remove those small RP values from the rp vector and conf matrix
  107. rp_idx_keep = rp > rp_reject
  108. rp = rp[rp_idx_keep]
  109. confMatrix = confMatrix[:, rp_idx_keep]
  110. return confMatrix, rp
  111. def confidence_contamin(confMatrix, cont, rp, cont_thresh=10.0, rp_reject = 0.0005):
  112. '''
  113. For a level of contamination contamin_level given (default 10%), find the smallest confidence
  114. value for which the minimum value of the contamination curve is equal or lower to the
  115. contamin_level.
  116. In practice, this is equivalent to finding the maximum value of the confidence of
  117. the confidence matrix at the contamination level row.
  118. Uses the output of the function computeMatrix()
  119. :param confMatrix: the confidence matrix (contamination x RP, values: confidence, ranging from 0-1)
  120. :param cont: contamination vector at which the confidence is computed (ranges by default from 0-35)
  121. :param rp: refractory period vector at which the confidence is computed
  122. :param cont_level: level of contamination searched for, default is 10% (0.1)
  123. :return:
  124. '''
  125. # Find index in cont vector where there is cont_thresh or closest (higher) value
  126. idx_cont = np.where(cont >= cont_thresh)[0][0]
  127. cont_thresh = cont[idx_cont] # Return actual level of contamination studied
  128. # We want to compute the curve of contamination only for RPs above a certain value
  129. # Remove those small RP values from the rp vector and conf matrix
  130. confMatrix, _ = remove_lowrp_confmat(confMatrix, rp, rp_reject=rp_reject)
  131. # At the contamination level studied, find the maximal value of confidence
  132. max_conf = np.max(confMatrix[idx_cont, :]) # Legacy name: 'maxConfidenceAt10Cont'
  133. return max_conf, idx_cont, cont_thresh
  134. def pass_slidingRP_confmat(confMatrix, cont, rp, conf_thresh=90, cont_thresh=10, rp_reject=0.0005):
  135. '''
  136. Given a confidence matrix, a confidence threshold (default 90%) and a contamination threshold (default=10),
  137. assess whether the unit passes the sliding RP metric
  138. Uses the output of the function computeMatrix()
  139. :param confMatrix:
  140. :param cont:
  141. :param rp:
  142. :param conf_thresh:
  143. :param cont_thresh:
  144. :param rp_reject:
  145. :return:
  146. '''
  147. # We want to compute the curve of contamination only for RPs above a certain value
  148. # Remove those small RP values from the rp vector and conf matrix
  149. confMatrix, rp = remove_lowrp_confmat(confMatrix, rp, rp_reject=rp_reject)
  150. # Find matrix indices that are above or equal to the confidence threshold
  151. a = np.where(confMatrix >= conf_thresh)
  152. if len(a[0]) > 0:
  153. # Find minimum contamination value for this conf threshold
  154. min_idx = np.min(a[0]) # Min on rows axis = contamination axis
  155. min_cont = cont[min_idx] # Legacy name: minContWith90Confidence
  156. # Find the smallest RP possible at the contamination level at the max confidence val
  157. minRP = np.argmax(confMatrix[min_idx, :])
  158. rp_min_val = rp[minRP] # Legacy name: timeOfLowestCont
  159. # rp_min_val is the tau_r at which the minimum contamination is confirmed,
  160. # i.e. an estimate of the unit's RP duration (manuscript Outputs, Step 6).
  161. # Check if this unit passes the sliding RP metric
  162. pass_cont_thresh = min_cont <= cont_thresh
  163. else:
  164. pass_cont_thresh = False
  165. min_cont = np.nan
  166. rp_min_val = np.nan
  167. return pass_cont_thresh, min_cont, rp_min_val # Legacy: value, minContWith90Confidence, timeOfLowestCont
  168. def slidingRP(spikeTimes, params=None, conf_thresh=90, cont_thresh=10, rp_reject=0.0005):
  169. """Compute the Sliding RP metric for a single cluster.
  170. Mirrors the MATLAB slidingRP.m. Pass options in the ``params`` dict
  171. (e.g. ``params={'recDur': 3600}``); recDur is the recording duration in
  172. seconds and defaults to ``max(spikeTimes)`` if not given (recommended to
  173. set explicitly). The ACG and expected-violation (Llobet) calculation are
  174. identical to the MATLAB implementation.
  175. The maximum confidence and pass/fail come from the per-tau_r confidence at
  176. the contamination threshold; the minimum confirmable contamination
  177. (``min_cont``) is computed analytically (closed-form, no contamination grid).
  178. For the full confidence matrix, call computeMatrix() directly.
  179. params options include: recDur, sampleRate, binSizeCorr, cont, correction
  180. (FWER multiple-comparisons; default off), and forcePass (IBL-specific; default
  181. off) — when True, units with zero <2 ms violations and firing_rate > 0.5 are
  182. flagged via pass_forced.
  183. Returns
  184. -------
  185. max_conf : float maximum confidence (%) that contamination < cont_thresh
  186. min_cont : float minimum contamination (%) confirmable at conf_thresh
  187. (continuous; NaN if > 35%)
  188. rp_min_val : float tau_r (s) at which min_cont is achieved
  189. n_spikes_below2 : int ACG count with ISI < 2 ms
  190. firing_rate : float n_spikes / recDur
  191. pass_cont_thresh : bool max_conf >= conf_thresh
  192. pass_forced : bool IBL force-pass flag (False unless params['forcePass'])
  193. Mapping to MATLAB slidingRP.m outputs (which differ in order):
  194. MATLAB [passTest, confidence, contamination, timeOfLowestCont, nViolShort, ...]
  195. Python pass_cont_thresh, max_conf, min_cont, rp_min_val, n_spikes_below2
  196. (MATLAB's confMatrix/cont/rp/nACG come from computeMatrix(); Python returns
  197. firing_rate and pass_forced instead. Values match; only order/names differ.)
  198. """
  199. params = dict(params) if params else {}
  200. sampleRate = params.setdefault('sampleRate', 30000)
  201. rpBinSize = params.setdefault('binSizeCorr', 1 / sampleRate)
  202. spikeTimes = np.asarray(spikeTimes, dtype=np.float64)
  203. if params.get('correction', False):
  204. # FWER-corrected variant (Fig. S3): derive the scalar metrics from the
  205. # corrected confidence matrix (grid-based; no analytical shortcut for the
  206. # corrected confidence). Expensive, non-default path.
  207. confMatrix, cont, rp, nACG, firing_rate = computeMatrix(spikeTimes, params)
  208. pass_cont_thresh, min_cont, rp_min_val = pass_slidingRP_confmat(
  209. confMatrix, cont, rp, conf_thresh, cont_thresh, rp_reject)
  210. max_conf, _, _ = confidence_contamin(confMatrix, cont, rp, cont_thresh, rp_reject)
  211. n_spikes_below2 = int(np.sum(nACG[0:np.where(rp > 0.002)[0][0] + 1]))
  212. pass_forced = params.get('forcePass', False) and (n_spikes_below2 == 0) \
  213. and (firing_rate > 0.5) and (not pass_cont_thresh)
  214. return max_conf, min_cont, rp_min_val, n_spikes_below2, firing_rate, \
  215. pass_cont_thresh, pass_forced
  216. n_spikes = spikeTimes.size
  217. recDur = params.get('recDur', None)
  218. if recDur is None:
  219. recDur = float(np.max(spikeTimes)) if n_spikes else 0.0
  220. rpEdges = np.arange(0, 10 / 1000, rpBinSize) # in s
  221. rp = rpEdges + rpBinSize / 2 # bin centres (s)
  222. refDur = rp + rpBinSize / 2 # right bin edge = tested tau_r
  223. nACG = computeACG(spikeTimes, rpBinSize, rp.size)
  224. obsViol = np.cumsum(nACG)
  225. firing_rate = n_spikes / recDur if recDur > 0 else 0.0
  226. testTimes = rp > rp_reject # exclude tau_r below rp_reject from pass/fail
  227. # Max confidence at the contamination threshold (identical to the matrix path)
  228. conf_at_thresh = 100 * computeViol(obsViol, firing_rate, n_spikes, refDur,
  229. cont_thresh / 100, recDur)
  230. max_conf = float(np.max(conf_at_thresh[testTimes])) if np.any(testTimes) else 0.0
  231. # Minimum confirmable contamination, computed analytically (continuous)
  232. min_cont, rp_min_val = compute_min_contamination(
  233. obsViol, n_spikes, refDur, rp, recDur, conf_thresh, rp_reject)
  234. pass_cont_thresh = bool(max_conf >= conf_thresh)
  235. n_spikes_below2 = int(np.sum(nACG[0:np.where(rp > 0.002)[0][0] + 1]))
  236. # IBL-specific: force-pass units with zero short-ISI violations and FR > 0.5
  237. # (tuned for IBL ~1 h recordings). Opt-in only (default off), matching the
  238. # correction path and the docstring.
  239. pass_forced = params.get('forcePass', False) and (n_spikes_below2 == 0) \
  240. and (firing_rate > 0.5) and (not pass_cont_thresh)
  241. return max_conf, min_cont, rp_min_val, \
  242. n_spikes_below2, firing_rate, \
  243. pass_cont_thresh, pass_forced
  244. def _slidingRP_worker(args):
  245. """Module-level worker so slidingRP_all can parallelise with a process pool
  246. (the callable and its args must be picklable)."""
  247. st, params, conf_thresh, cont_thresh, rp_reject = args
  248. return slidingRP(st, params=params, conf_thresh=conf_thresh,
  249. cont_thresh=cont_thresh, rp_reject=rp_reject)
  250. def slidingRP_all(spikeTimes, spikeClusters, params=None,
  251. conf_thresh=90, cont_thresh=10, rp_reject=0.0005, n_jobs=1):
  252. """Compute the Sliding RP metric for every cluster in a recording.
  253. :param spikeTimes: array of spike times (s)
  254. :param spikeClusters: array of spike cluster ids that corresponds to spikeTimes
  255. :param params: dict of options passed to slidingRP (e.g. {'recDur': ...})
  256. :param n_jobs: number of parallel processes over clusters. 1 (default) runs
  257. serially; >1 uses that many processes; <0 uses all cores.
  258. (Process-based, like the MATLAB parfor; on Windows the caller
  259. must be under ``if __name__ == '__main__'``.)
  260. :return: dictionary of per-cluster metrics
  261. """
  262. cids = np.unique(spikeClusters)
  263. # Pre-slice spikes per cluster (avoids re-scanning the full arrays per task).
  264. sts = [spikeTimes[spikeClusters == c] for c in cids]
  265. if n_jobs is not None and n_jobs != 1 and len(cids) > 1:
  266. from concurrent.futures import ProcessPoolExecutor
  267. max_workers = None if n_jobs < 0 else n_jobs
  268. args = [(st, params, conf_thresh, cont_thresh, rp_reject) for st in sts]
  269. with ProcessPoolExecutor(max_workers=max_workers) as ex:
  270. results = list(ex.map(_slidingRP_worker, args))
  271. else:
  272. results = [slidingRP(st, params=params, conf_thresh=conf_thresh,
  273. cont_thresh=cont_thresh, rp_reject=rp_reject)
  274. for st in sts]
  275. rpMetrics = {k: [] for k in ('cidx', 'max_confidence', 'min_contamination',
  276. 'rp_min_val', 'n_spikes_below2', 'firing_rate',
  277. 'value', 'value_forced')}
  278. for cid, res in zip(cids, results):
  279. (max_confidence, min_contamination, rp_min_val, n_spikes_below2,
  280. firing_rate, pass_cont_thresh, pass_forced) = res
  281. rpMetrics['cidx'].append(cid)
  282. rpMetrics['max_confidence'].append(max_confidence)
  283. rpMetrics['min_contamination'].append(min_contamination)
  284. rpMetrics['rp_min_val'].append(rp_min_val)
  285. rpMetrics['n_spikes_below2'].append(n_spikes_below2)
  286. rpMetrics['firing_rate'].append(firing_rate)
  287. rpMetrics['value'].append(int(pass_cont_thresh))
  288. rpMetrics['value_forced'].append(int(pass_forced))
  289. return rpMetrics
  290. ## Code from OW
  291. def computeACG(spikeTimes, rpBinSize, nBins):
  292. """Autocorrelogram by histogramming pairwise spike-time differences.
  293. This replicates cortex-lab `histdiff` (used by the MATLAB implementation)
  294. exactly: bin index = floor((t_i - t_j) / rpBinSize), counting each ordered
  295. pair (j < i) whose difference lies in (0, rpBinSize*nBins). Exact
  296. coincidences (diff == 0) are excluded. Implemented with the shifted-train
  297. trick so only nearby pairs are examined.
  298. Parameters
  299. ----------
  300. spikeTimes : numpy.ndarray
  301. Spike times in seconds (need not be sorted).
  302. rpBinSize : float
  303. ACG bin width in seconds (the recording sample period, 1/sampleRate).
  304. nBins : int
  305. Number of ACG bins (the window is rpBinSize * nBins seconds).
  306. Returns
  307. -------
  308. nACG : numpy.ndarray (nBins,)
  309. Integer count of spike pairs with ISI in each bin.
  310. """
  311. st = np.sort(np.asarray(spikeTimes, dtype=np.float64))
  312. max_lag = rpBinSize * nBins
  313. counts = np.zeros(nBins, dtype=np.int64)
  314. n = st.size
  315. shift = 1
  316. # Differences grow with shift, so once none fall within the window we stop.
  317. while shift < n:
  318. dt = st[shift:] - st[:-shift]
  319. m = (dt > 0) & (dt < max_lag)
  320. if not np.any(m):
  321. break
  322. bins = np.floor(dt[m] / rpBinSize).astype(np.int64)
  323. counts += np.bincount(bins, minlength=nBins)[:nBins]
  324. shift += 1
  325. return counts
  326. def computeMatrix(spikeTimes, params):
  327. """Build the [nCont x nRP] confidence matrix for one cluster.
  328. Mirrors matlab/computeMatrix.m. The ACG is computed by computeACG (a
  329. histdiff-equivalent), and expected violations use the Llobet formula with
  330. an explicit recording duration.
  331. Parameters
  332. ----------
  333. spikeTimes : numpy.ndarray
  334. array of spike times (s)
  335. params : dict
  336. - recDur : recording duration (s). Defaults to max(spikeTimes);
  337. recommended to set explicitly.
  338. - binSizeCorr : ACG bin size (s), default 1/sampleRate.
  339. - sampleRate : sample rate (Hz), default 30000.
  340. - cont : vector of contamination levels (%) to test, default 0.5:0.5:35.
  341. - correction : bool, default False. If True, apply the family-wise
  342. multiple-comparisons correction across tau_r (exact Poisson
  343. first-passage; Fig. S3). Slow and over-conservative for short-RP
  344. units; intended for Fig. S3 only.
  345. - rpReject : min tau_r (s) included in the correction, default 0.0005.
  346. Returns
  347. -------
  348. confMatrix : [nCont x nRP] confidence (%) that contamination < cont(i) at rp(j)
  349. cont : tested contamination levels (%)
  350. rp : tested refractory-period durations (s, bin centres)
  351. nACG : ACG counts per bin
  352. firingRate : spike count / recDur (spks/s)
  353. """
  354. sampleRate = params.get('sampleRate', 30000)
  355. rpBinSize = params.get('binSizeCorr', 1 / sampleRate)
  356. recDur = params.get('recDur', None)
  357. if recDur is None:
  358. recDur = np.max(spikeTimes)
  359. # contamination levels (%): 0.5,1,...,35 (70 levels), matching MATLAB
  360. # 0.5:0.5:35 and the manuscript (Python previously stopped at 34.5).
  361. cont = params.get('cont', np.arange(0.5, 35.5, 0.5))
  362. correction = params.get('correction', False)
  363. rp_reject = params.get('rpReject', 0.0005)
  364. rpEdges = np.arange(0, 10 / 1000, rpBinSize) # in s
  365. rp = rpEdges + rpBinSize / 2 # refractory period durations to test (bin centres)
  366. n_spikes = spikeTimes.size
  367. firingRate = n_spikes / recDur
  368. nACG = computeACG(spikeTimes, rpBinSize, rp.size)
  369. obsViol = np.cumsum(nACG[0:rp.size])
  370. refDur = rp + rpBinSize / 2
  371. # Pointwise (nominal) confidence matrix
  372. Nc = n_spikes * (cont[:, np.newaxis] / 100)
  373. Nb = n_spikes * (1 - cont[:, np.newaxis] / 100)
  374. expectedViolMatrix = 2 * refDur[np.newaxis, :] / recDur * Nc * (Nb + (Nc - 1) / 2)
  375. nominalConfMatrix = 100 * (1 - stats.poisson.cdf(obsViol[np.newaxis, :], expectedViolMatrix))
  376. if not correction:
  377. confMatrix = nominalConfMatrix
  378. else:
  379. confMatrix = _fwer_correct(nominalConfMatrix, expectedViolMatrix,
  380. obsViol, rp, rp_reject)
  381. return confMatrix, cont, rp, nACG, firingRate
  382. def _fwer_correct(nominalConfMatrix, expectedViolMatrix, obsViol, rp, rp_reject):
  383. """Family-wise-error-rate correction across tau_r (exact Poisson
  384. first-passage / Markov-chain DP). Port of matlab/computeMatrix.m's
  385. correction branch. Returns the corrected confidence matrix (%)."""
  386. nCont, nRP = nominalConfMatrix.shape
  387. corrected = np.full((nCont, nRP), np.nan)
  388. validIdx = np.where(rp > rp_reject)[0]
  389. if validIdx.size == 0:
  390. corrected[:] = 0
  391. return corrected
  392. for cidx in range(nCont):
  393. expectedViol = expectedViolMatrix[cidx, :]
  394. nomPvals = 1 - nominalConfMatrix[cidx, :] / 100
  395. minPval = np.min(nomPvals[validIdx])
  396. # Shortcut: the corrected confidence cannot exceed (1 - minPval)
  397. if minPval > 0.5:
  398. corrected[cidx, :] = (1 - minPval) * 100
  399. continue
  400. # Per-bin passing boundary: max observed count with pointwise p <= minPval
  401. c = -np.ones(nRP, dtype=np.int64)
  402. expV_valid = expectedViol[validIdx]
  403. obsV_valid = obsViol[validIdx]
  404. max_obs = int(np.max(obsV_valid))
  405. if max_obs == 0:
  406. c[validIdx] = (np.exp(-expV_valid) <= minPval + 1e-10).astype(np.int64) - 1
  407. else:
  408. v_vec = np.arange(max_obs + 1)[:, np.newaxis]
  409. cdf = stats.poisson.cdf(v_vec, expV_valid[np.newaxis, :])
  410. valid_mask = (v_vec <= obsV_valid[np.newaxis, :]) & (cdf <= minPval + 1e-10)
  411. c[validIdx] = valid_mask.sum(axis=0) - 1
  412. max_c = int(np.max(c[validIdx]))
  413. # Markov-chain DP for the first-passage probability
  414. if max_c < 0:
  415. correctedConf = 1.0
  416. else:
  417. P_state = np.zeros(max_c + 1)
  418. P_state[0] = 1.0
  419. P_false = 0.0
  420. lambda_all = np.concatenate(([expectedViol[0]], np.diff(expectedViol)))
  421. for k in range(nRP):
  422. lam = lambda_all[k]
  423. if lam > 0:
  424. pmf = stats.poisson.pmf(np.arange(max_c + 1), lam)
  425. P_state = np.convolve(P_state, pmf)[:max_c + 1]
  426. if c[k] >= 0:
  427. P_false += P_state[:c[k] + 1].sum()
  428. P_state[:c[k] + 1] = 0
  429. correctedConf = 1 - P_false
  430. corrected[cidx, :] = correctedConf * 100
  431. return corrected
  432. def computeViol(obsViol, firingRate, spikeCount, refDur, contaminationProp, recDur):
  433. '''Poisson confidence score for a single (refDur, contamination) hypothesis.
  434. Matches matlab/computeViol.m. Expected violations follow the Llobet et al.
  435. (2022) formulation, in which contaminating spikes produce violations both
  436. with base-neuron spikes and with each other:
  437. Ve = 2 * refDur / recDur * Nc * (Nb + (Nc - 1) / 2)
  438. where Nc = C * N_total and Nb = (1 - C) * N_total.
  439. Parameters
  440. ----------
  441. obsViol : int or array
  442. observed number of violations (cumulative ACG count up to refDur).
  443. firingRate : float
  444. accepted for API parity with the MATLAB signature; not used (recDur
  445. is used directly). Pass None.
  446. spikeCount : int
  447. total spike count of the cluster (N_total).
  448. refDur : float or array
  449. refractory period duration tested, tau_r (seconds).
  450. contaminationProp : float or array
  451. hypothesised contamination as a proportion in [0, 1].
  452. recDur : float
  453. recording duration D (seconds).
  454. Returns
  455. -------
  456. confidenceScore : float or array
  457. probability that true contamination is below contaminationProp given
  458. the observed violations, under a Poisson assumption. 1 - Poisson_CDF.
  459. '''
  460. Nc = spikeCount * contaminationProp
  461. Nb = spikeCount * (1 - contaminationProp)
  462. expectedViol = 2 * refDur / recDur * Nc * (Nb + (Nc - 1) / 2)
  463. confidenceScore = 1 - stats.poisson.cdf(obsViol, expectedViol)
  464. return confidenceScore
  465. def compute_min_contamination(obsViol, spikeCount, refDur, rp, recDur,
  466. conf_thresh=90, rp_reject=0.0005, max_contam=35.0):
  467. """Analytical minimum contamination confirmable at the confidence threshold.
  468. Closed-form equivalent of scanning the contamination grid (cf. Ressmeyer's
  469. analytical method, PR #6), specialised to the exact Llobet Ve. For each
  470. tau_r, the critical Poisson rate at which the confidence equals conf_thresh
  471. is obtained via the Poisson<->chi-squared identity:
  472. lambda_crit = chi2.ppf(conf_thresh/100, 2*(V_o + 1)) / 2
  473. Inverting Ve(C) = lambda_crit (Ve = 2*tau/D * C*N * ((1-C)*N + (C*N-1)/2))
  474. gives the smaller root
  475. C_min(tau) = [ (N - 0.5) - sqrt((N - 0.5)^2 - lambda_crit*D/tau) ] / N.
  476. The unit's minimum confirmable contamination is min_tau C_min(tau) over
  477. tau_r > rp_reject.
  478. Parameters
  479. ----------
  480. obsViol : array cumulative observed violations V_o(tau_r).
  481. spikeCount : int total spikes N.
  482. refDur : array tested tau_r (s) = right bin edge.
  483. rp : array bin-centre tau_r (s), used for the rp_reject mask and
  484. the returned rp_min_val.
  485. recDur : float recording duration D (s).
  486. conf_thresh : float confidence threshold (%).
  487. rp_reject : float exclude tau_r at or below this (s).
  488. max_contam : float cap (%); returns NaN if the minimum exceeds it.
  489. Returns
  490. -------
  491. min_cont : float minimum confirmable contamination (%), or NaN.
  492. rp_min_val : float tau_r (s) at which it is achieved, or NaN.
  493. """
  494. N = spikeCount
  495. if N == 0 or recDur <= 0:
  496. return np.nan, np.nan
  497. gamma = conf_thresh / 100
  498. lam = stats.chi2.ppf(gamma, 2 * (obsViol + 1)) / 2
  499. disc = (N - 0.5) ** 2 - lam * recDur / refDur
  500. Cmin = np.full(refDur.shape, np.nan)
  501. ok = disc >= 0
  502. Cmin[ok] = ((N - 0.5) - np.sqrt(disc[ok])) / N * 100 # as a percentage
  503. testTimes = rp > rp_reject
  504. Cmin_test = Cmin[testTimes]
  505. if not np.any(testTimes) or np.all(np.isnan(Cmin_test)):
  506. return np.nan, np.nan
  507. idx = int(np.nanargmin(Cmin_test))
  508. min_cont = float(Cmin_test[idx])
  509. if min_cont > max_contam:
  510. return np.nan, np.nan
  511. rp_min_val = float(rp[testTimes][idx])
  512. return min_cont, rp_min_val
  513. def plot_acg(ax, acg, timeBins, estimatedIdx=None):
  514. # Convert time in milliseconds for plotting
  515. timeBins = timeBins * 1000
  516. if len(acg) > len(timeBins):
  517. acg = acg[0:len(timeBins)]
  518. # TODO change this so the correct bins are taken for whatever values of timeBins
  519. ax.bar(timeBins, acg, width=np.diff(timeBins)[0], alpha=0.5)
  520. ax.set_xlim(0, timeBins[-1])
  521. ax.spines['right'].set_visible(False)
  522. ax.spines['top'].set_visible(False)
  523. if estimatedIdx is not None:
  524. # Plot RP as black vertical line
  525. ax.plot([timeBins[estimatedIdx], timeBins[estimatedIdx]], [0, max(acg)], 'k-')
  526. ax.set_ylabel('Number of spikes')
  527. ax.set_xlabel('Time (ms)')
  528. def plotSigmoid(ax, acg, timeBins, ySigmoid, estimatedIdx, estimatedRP):
  529. plot_acg(ax, acg, timeBins, estimatedIdx=estimatedIdx)
  530. # Plot on top of ACG the sigmoid
  531. ax.plot(timeBins[0:len(ySigmoid)] * 1000, ySigmoid, 'b')
  532. ax.plot(timeBins[estimatedIdx] * 1000, ySigmoid[estimatedIdx], 'rx')
  533. ax.set_title('Estimated RP:%.2f ms' % estimatedRP)
  534. def plotSlidingRP(spikeTimes, params=None, plotXs=None, inputAxes=None,
  535. plotExtraContours=False):
  536. """Visualise the Sliding RP result for one cluster (Python twin of
  537. matlab/plotSlidingRP.m). Produces three panels:
  538. 1. ACG (0-5 ms),
  539. 2. confidence matrix (contamination x tau_r) with the 90% iso-contour and
  540. the minimum-contamination time,
  541. 3. confidence trace at the contamination threshold (10%), optionally also
  542. at 7.5% and 15% (plotExtraContours).
  543. Parameters
  544. ----------
  545. spikeTimes : array of spike times (s).
  546. params : dict, optional. Recognised keys: sampleRate, binSizeCorr, recDur,
  547. contaminationThresh (default 10), confidenceThresh (default 90),
  548. savefig (bool), figpath (str, no extension).
  549. plotXs : [taus_ms, colors] optional. Vertical markers at the given tau_r
  550. values (ms) in the matching colors, drawn on panels 1 and 3.
  551. inputAxes : (fig, axs) optional, where axs has at least 3 axes; otherwise a
  552. new 1x3 figure is created.
  553. plotExtraContours : bool. Also draw the 7.5% and 15% confidence traces.
  554. Returns the (fig, axs) used.
  555. """
  556. import matplotlib.pyplot as plt
  557. params = dict(params) if params else {}
  558. contThresh = params.get('contaminationThresh', 10)
  559. confThresh = params.get('confidenceThresh', 90)
  560. confMatrix, cont, rp, nACG, firingRate = computeMatrix(np.asarray(spikeTimes), params)
  561. _, _, rp_min_val, _, _, _, _ = slidingRP(
  562. spikeTimes, params=params, conf_thresh=confThresh, cont_thresh=contThresh)
  563. if inputAxes is not None:
  564. fig, axs = inputAxes
  565. else:
  566. fig, axs = plt.subplots(1, 3, figsize=(12, 4))
  567. rp_ms = rp * 1000
  568. # --- Panel 1: ACG ---
  569. ax = axs[0]
  570. ax.bar(rp_ms, nACG[0:len(rp)], width=np.diff(rp_ms)[0], color='k')
  571. ax.set_xlim([0, 5])
  572. ax.set_xlabel('Time from spike (ms)')
  573. ax.set_ylabel('ACG count (spks)')
  574. ax.fill(np.array([0, 1, 1, 0]) * 0.5, np.array([0, 0, 1, 1]) * ax.get_ylim()[1],
  575. 'k', alpha=0.2)
  576. if plotXs is not None:
  577. for tau_ms, c in zip(plotXs[0], plotXs[1]):
  578. ax.axvline(tau_ms, color=c, linewidth=1)
  579. ax.spines['right'].set_visible(False)
  580. ax.spines['top'].set_visible(False)
  581. # --- Panel 2: confidence matrix ---
  582. ax = axs[1]
  583. im = ax.imshow(confMatrix, extent=[rp_ms[0], rp_ms[-1], cont[0], cont[-1]],
  584. aspect='auto', vmin=0, vmax=100, origin='lower')
  585. cbar = fig.colorbar(im, ax=ax)
  586. cbar.set_label('Confidence (%)')
  587. ax.plot([rp_ms[0], rp_ms[-1]], [contThresh, contThresh], 'r', linewidth=1)
  588. if not np.isnan(rp_min_val):
  589. ax.plot([rp_min_val * 1000] * 2, [cont[0], cont[-1]], 'r', linewidth=1)
  590. # 90% iso-contour
  591. z = np.vstack([np.zeros((1, confMatrix.shape[1])), confMatrix])
  592. ii = np.argmax(z > confThresh, axis=0).astype(float)
  593. ii[ii == 0] = np.nan
  594. contContour = np.full(ii.shape, np.nan)
  595. good = ~np.isnan(ii)
  596. contContour[good] = cont[(ii[good] - 1).astype(int)]
  597. ax.plot(rp_ms, contContour, 'r', linewidth=2)
  598. ax.set_xlim([0, 5])
  599. ax.set_xlabel('Time from spike (ms)')
  600. ax.set_ylabel('Contamination (%)')
  601. ax.invert_yaxis()
  602. # --- Panel 3: confidence trace(s) ---
  603. ax = axs[2]
  604. def _trace(level, **kw):
  605. idx = np.argmin(np.abs(cont - level))
  606. ax.plot(rp_ms, confMatrix[idx, :], **kw)
  607. if plotExtraContours:
  608. _trace(7.5, color='lightcoral', linewidth=1, label='7.5%')
  609. _trace(15, color='darkred', linewidth=1, label='15%')
  610. _trace(contThresh, color='r', linewidth=2, label='%g%%' % contThresh)
  611. ax.plot([0, 5], [confThresh, confThresh], 'k', linewidth=1)
  612. if plotXs is not None:
  613. for tau_ms, c in zip(plotXs[0], plotXs[1]):
  614. ax.axvline(tau_ms, color=c, linewidth=1)
  615. ax.set_xlim([0, 5])
  616. ax.set_ylim([0, 100])
  617. ax.set_xlabel('Time from spike (ms)')
  618. ax.set_ylabel('Confidence of <=%g%% contamination (%%)' % contThresh)
  619. ax.legend(frameon=False, fontsize=8)
  620. ax.spines['right'].set_visible(False)
  621. ax.spines['top'].set_visible(False)
  622. fig.tight_layout()
  623. if params.get('savefig', False) and 'figpath' in params:
  624. fig.savefig(params['figpath'] + '.svg', dpi=300)
  625. fig.savefig(params['figpath'] + '.png', dpi=300)
  626. return fig, axs
  627. # return acg
  628. #
  629. # # helper functions
  630. #
  631. #
  632. # def find_nearest(array, value):
  633. # array = np.asarray(array)
  634. # subtracted = (array - value)
  635. # valid_idx = np.where(subtracted >= 0)[0]
  636. # if len(valid_idx) > 0:
  637. #
  638. # out = valid_idx[subtracted[valid_idx].argmin()]
  639. #
  640. # else:
  641. # out = np.nan
  642. #
  643. # return out
  644. #
  645. #

metrics.py at commit bb841e5, no license · at the source

Overview

  1. Department of Neurobiology & Biophysics, University of Washington, Seattle, WA, USA
  2. International Brain Laboratory
  3. Department of Bioengineering, University of Washington, Seattle, WA, USA
Institutions: University of Washington (United States); International Brain Laboratory (Portugal)
Dates: published online 9 March 2026
Type: Preprint · Language: English
License: CC BY-NC
Identifiers: DOI 10.64898/2026.03.06.710130 · OpenAlex W7135018293
Open access: green, a free copy (OpenAlex)
Status: code verified
Categories: methods / tools (subfield)
Methods: Statistics, Preprocessing, Connectivity, fMRI & imaging, Single-unit activity, calcium imaging, Physiology & signal measures
Topic: Neural dynamics and brain function (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Wellcome Trust (216324/Z/19/Z)
Citations: not cited yet (Europe PMC); 27 references in the paper

Abstract

The increasing size of electrophysiological datasets has heightened the need for quality metrics that automatically reject neurons whose activity was recorded with low sensitivity or specificity. One key approach estimates artifactual contamination by assuming that each neuron has a refractory period (RP), a brief time interval following each action potential when further activity cannot occur. However, existing methods cannot be applied without prior knowledge of the neurons’ RP durations, limiting their usefulness in datasets that include neurons from brain regions or species in which RP durations have not been systematically characterized. Here, we find that neurons in some brain regions (thalamus) and species (macaque) have shorter RP durations than commonly assumed, and we introduce a new metric, the Sliding Refractory Period metric, which is robust to variation in a neuron’s RP duration without tuning. We validate the method using simulations, demonstrating that it improves acceptance of uncontaminated spike trains with short or long RP durations while still rejecting contaminated ones. Moreover, by incorporating Poisson statistics into the calculation, the method also improves on prior work by allowing the user to approximately control the false acceptance rate. Our new metric improves quantification of contamination in electrophysiological recordings and enables application of a single tuning-free quality metric to data recorded from diverse brain regions and species.

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

Repository

Its files are read in the Code ↔ Paper reader above, with 16 matches between paragraphs and lines of code.

SteinmetzLab/slidingRefractory

License: none: the authors keep all their rights
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: bb841e55defb9cb95242a8177d50de1916c2a2e2, 16 July 2026
Languages: MATLAB (24), Python (15)
Size: 62 files, 39 scripts
Software Heritage: not archived
Found in: “Code and Data Availability”
Holds: README, CITATION.cff, environment (pyproject.toml), tests, continuous integration
Not found: license file, documentation
Tools: NumPy (12 files), Matplotlib (6 files), pandas (4 files), SciPy (4 files), Signal Processing Toolbox (2 files), Optimization Toolbox (1 file), Parallel Computing Toolbox (1 file), Statistics and Machine Learning Toolbox (1 file), Phy (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
40 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;
  • 39 scripts, each with its path and the digest of its content;
  • 16 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.

Code and Data Availability

Code for the metric and for the simulations presented is available at https://github.com/SteinmetzLab/slidingRefractory/.

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

Versions

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

Version 1, 30 September 2026: the first record

Recorded: type, language, journal, dates, 9 authors, 1 funder, 24 references.

Cite

This paper

Roth, N., Chapuis, G., Winter, O., International Brain Laboratory, Ressmeyer, R. A., Bun, L. M., Canfield, R. A., Horwitz, G. D., & Steinmetz, N. A. (2026). A flexible quality metric for electrophysiological recordings across brain regions and species. bioRxiv (preprint). https://doi.org/10.64898/2026.03.06.710130

BibTeX

@article{roth2026flexible,
author = {Roth, Noam and Chapuis, Gaelle and Winter, Olivier and {International Brain Laboratory} and Ressmeyer, Ryan A. and Bun, Luke M. and Canfield, Ryan A. and Horwitz, Gregory D. and Steinmetz, Nicholas A.},
title = {{A flexible quality metric for electrophysiological recordings across brain regions and species}},
journal = {bioRxiv (preprint)},
year = {2026},
month = mar,
publisher = {bioRxiv},
issn = {2692-8205},
doi = {10.64898/2026.03.06.710130},
url = {https://doi.org/10.64898/2026.03.06.710130}
}

RIS

TY - JOUR
AU - Roth, Noam
AU - Chapuis, Gaelle
AU - Winter, Olivier
AU - International Brain Laboratory
AU - Ressmeyer, Ryan A.
AU - Bun, Luke M.
AU - Canfield, Ryan A.
AU - Horwitz, Gregory D.
AU - Steinmetz, Nicholas A.
TI - A flexible quality metric for electrophysiological recordings across brain regions and species
T2 - bioRxiv (preprint)
J2 - bioRxiv
PY - 2026
DA - 2026/03/09
SN - 2692-8205
PB - bioRxiv
DO - 10.64898/2026.03.06.710130
UR - https://doi.org/10.64898/2026.03.06.710130
LA - en
ER -

CSL-JSON

{
"id": "10.64898/2026.03.06.710130",
"type": "article",
"title": "A flexible quality metric for electrophysiological recordings across brain regions and species",
"container-title": "bioRxiv (preprint)",
"author": [
{
"family": "Roth",
"given": "Noam"
},
{
"family": "Chapuis",
"given": "Gaelle"
},
{
"family": "Winter",
"given": "Olivier"
},
{
"literal": "International Brain Laboratory"
},
{
"family": "Ressmeyer",
"given": "Ryan A."
},
{
"family": "Bun",
"given": "Luke M."
},
{
"family": "Canfield",
"given": "Ryan A."
},
{
"family": "Horwitz",
"given": "Gregory D."
},
{
"family": "Steinmetz",
"given": "Nicholas A."
}
],
"container-title-short": "bioRxiv",
"DOI": "10.64898/2026.03.06.710130",
"ISSN": "2692-8205",
"publisher": "bioRxiv",
"URL": "https://doi.org/10.64898/2026.03.06.710130",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
9
]
]
}
}

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

Similar papers

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

[1] doi:10.1016/j.patter.2026.101590 [code]
Density-based longitudinal neuron tracking in high-density electrophysiological recordings.
Journal: Patterns (New York, N.Y.)
In common: Phy, Optimization Toolbox, Parallel Computing Toolbox, 7 other tools, 3 references
[2] doi:10.1038/s41593-026-02357-2 [code]
Experience reorganizes content-specific memory traces in macaques.
Journal: Nature neuroscience
In common: Phy, Optimization Toolbox, Parallel Computing Toolbox, 6 other tools, 1 reference
[3] doi:10.1038/s41467-026-75347-4 [code]
Sleep reveals dynamics integrating and segregating movement and stimulus representations in V1.
Journal: Nature communications
In common: Phy, Optimization Toolbox, Parallel Computing Toolbox, 6 other tools, 1 reference
[4] doi:10.1038/s41593-026-02232-0 [code]
Entorhinal cortex represents task-relevant remote locations independently of CA1.
Journal: Nature neuroscience
In common: Phy, Optimization Toolbox, Signal Processing Toolbox, 6 other tools, 1 reference
[5] 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: pandas, SciPy, Matplotlib, 1 other tool, 3 references, author Nicholas A. Steinmetz
[6] doi:10.1038/s41592-026-03076-z [code]
Neuropixels Opto: combining high-resolution electrophysiology and optogenetics.
Journal: Nature methods
In common: pandas, SciPy, Matplotlib, 1 other tool, 3 references, author Nicholas A. Steinmetz
[7] doi:10.1038/s41467-026-71725-0 [code]
Interactions across hemispheres in prefrontal cortex reflect global cognitive processing.
Journal: Nature communications
In common: Optimization Toolbox, Parallel Computing Toolbox, Signal Processing Toolbox, 5 other tools, 2 references
[8] doi:10.1523/jneurosci.2001-25.2026 [code]
Dynamics of Dentate Gyrus Place Cells and Dentate Spikes during Spatial and Nonspatial Changes in Environments.
Journal: The Journal of neuroscience : the official journal of the Society for Neuroscience
In common: Phy, Optimization Toolbox, Signal Processing Toolbox, 5 other tools, 1 reference
[9] doi:10.1038/s41467-026-71664-w [code]
Dorsal prefrontal cortex drives perseverative behavior in mice.
Journal: Nature communications
In common: Optimization Toolbox, Parallel Computing Toolbox, Signal Processing Toolbox, 2 other tools, 4 references
[10] doi:10.1126/sciadv.aeh7220 [code]
Central complex representations of self-movement are sufficient to compute wind direction in flight.
Journal: Science advances
In common: Optimization Toolbox, Parallel Computing Toolbox, Signal Processing Toolbox, 6 other tools

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.