• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

int-brain-lab / ibllib / 29037613829

09 Jul 2026 05:36PM UTC coverage: 61.575% (-3.3%) from 64.888%
29037613829

push

github

web-flow
Merge 88653ad51 into 087557586

1822 of 3048 new or added lines in 81 files covered. (59.78%)

3091 existing lines in 75 files now uncovered.

12243 of 19883 relevant lines covered (61.58%)

0.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

11.07
/brainbox/plot.py
1
"""
2
Plots metrics that assess quality of single units. Some functions here generate plots for the
3
output of functions in the brainbox `single_units.py` module.
4

5
Run the following to set-up the workspace to run the docstring examples:
6
>>> from brainbox import processing
7
>>> import one.alf.io as alfio
8
>>> import numpy as np
9
>>> import matplotlib.pyplot as plt
10
>>> import ibllib.ephys.spikes as e_spks
11
# (*Note, if there is no 'alf' directory, make 'alf' directory from 'ks2' output directory):
12
>>> e_spks.ks2_to_alf(path_to_ks_out, path_to_alf_out)
13
# Load the alf spikes bunch and clusters bunch, and get a units bunch.
14
>>> spks_b = alfio.load_object(path_to_alf_out, 'spikes')
15
>>> clstrs_b = alfio.load_object(path_to_alf_out, 'clusters')
16
>>> units_b = processing.get_units_bunch(spks_b)  # may take a few mins to compute
17
"""
18

19
import time
1✔
20
from warnings import warn
1✔
21

22
import matplotlib.pyplot as plt
1✔
23
import seaborn as sns
1✔
24
import numpy as np
1✔
25

26
# from matplotlib.ticker import StrMethodFormatter
27
from brainbox import singlecell
1✔
28
from brainbox.metrics import single_units
1✔
29
from brainbox.io.spikeglx import extract_waveforms
1✔
30
from iblutil.numerical import bincount2D
1✔
31
import spikeglx
1✔
32

33

34
def feat_vars(units_b, units=None, feat_name='amps', dist='norm', test='ks', cmap_name='coolwarm', ax=None):
1✔
35
    """
36
    Plots the coefficients of variation of a particular spike feature for all units as a bar plot,
37
    where each bar is color-coded corresponding to the depth of the max amplitude channel of the
38
    respective unit.
39

40
    Parameters
41
    ----------
42
    units_b : bunch
43
        A units bunch containing fields with spike information (e.g. cluster IDs, times, features,
44
        etc.) for all units.
45
    units : array-like (optional)
46
        A subset of all units for which to create the bar plot. (If `None`, all units are used)
47
    feat_name : string (optional)
48
        The spike feature to plot.
49
    dist : string (optional)
50
        The type of hypothetical null distribution from which the empirical spike feature
51
        distributions are presumed to belong to.
52
    test : string (optional)
53
        The statistical test used to calculate the probability that the empirical spike feature
54
        distributions come from `dist`.
55
    cmap_name : string (optional)
56
        The name of the colormap associated with the plot.
57
    ax : axessubplot (optional)
58
        The axis handle to plot the histogram on. (if `None`, a new figure and axis is created)
59

60
    Returns
61
    -------
62
    cv_vals : ndarray
63
        The coefficients of variation of `feat_name` for each unit.
64
    p_vals : ndarray
65
        The probabilites that the distribution for `feat_name` for each unit comes from a
66
        `dist` distribution based on the `test` statistical test.
67

68
    See Also
69
    --------
70
    metrics.unit_stability
71

72
    Examples
73
    --------
74
    1) Create a bar plot of the coefficients of variation of the spike amplitudes for all units.
75
        >>> fig, var_vals, p_vals = bb.plot.feat_vars(units_b)
76
    """
77

78
    # Get units.
NEW
79
    if units is not None:  # we're using a subset of all units
×
UNCOV
80
        unit_list = list(units_b['depths'].keys())
×
81
        # For each unit in `unit_list`, remove unit from `units_b` if not in `units`.
NEW
82
        [units_b['depths'].pop(unit) for unit in unit_list if int(unit) not in units]
×
UNCOV
83
    unit_list = list(units_b['depths'].keys())  # get new `unit_list` after removing unit
×
84

85
    # Calculate coefficients of variation for all units
NEW
86
    p_vals_b, cv_b = single_units.unit_stability(units_b, units=units, feat_names=[feat_name], dist=dist, test=test)
×
87
    cv_vals = np.array(tuple(cv_b[feat_name].values()))
×
UNCOV
88
    cv_vals = cv_vals * 1e6 if feat_name == 'amps' else cv_vals  # convert to uV if amps
×
UNCOV
89
    p_vals = np.array(tuple(p_vals_b[feat_name].values()))
×
90

91
    # Remove any empty units. This must be done AFTER the above calculations for ALL units so that
92
    # we can keep direct indexing.
UNCOV
93
    empty_unit_idxs = np.where([len(units_b['times'][unit]) == 0 for unit in unit_list])[0]
×
UNCOV
94
    good_units = [unit for unit in unit_list if unit not in empty_unit_idxs.astype(str)]
×
95

96
    # Get mean depths of spikes for good units
UNCOV
97
    depths = np.asarray([np.mean(units_b['depths'][str(unit)]) for unit in good_units])
×
98

99
    # Create unit normalized colormap based on `depths`, sorted by depth.
100
    cmap = plt.cm.get_cmap(cmap_name)
×
UNCOV
101
    depths_norm = depths / np.max(depths)
×
UNCOV
102
    rgba = np.asarray([cmap(depth) for depth in np.sort(np.flip(depths_norm))])
×
103

104
    # Plot depth-color-coded h bar plot of CVs for `feature` for each unit, where units are
105
    # sorted descendingly by depth along y-axis.
106
    if ax is None:
×
107
        fig, ax = plt.subplots()
×
108
    ax.barh(y=[int(unit) for unit in good_units], width=cv_vals[np.argsort(depths)], color=rgba)
×
109
    fig = ax.figure
×
110
    cbar = fig.colorbar(plt.cm.ScalarMappable(cmap=cmap), ax=ax)
×
111
    max_d = np.max(depths)
×
112
    tick_labels = [int(max_d * tick) for tick in (0, 0.2, 0.4, 0.6, 0.8, 1.0)]
×
113
    cbar.set_ticks(cbar.get_ticks())  # must call `set_ticks` to call `set_ticklabels`
×
114
    cbar.set_ticklabels(tick_labels)
×
115
    ax.set_title('CV of {feat}'.format(feat=feat_name))
×
116
    ax.set_ylabel('Unit Number (sorted by depth)')
×
UNCOV
117
    ax.set_xlabel('CV')
×
118
    cbar.set_label('Depth', rotation=-90)
×
119

UNCOV
120
    return cv_vals, p_vals
×
121

122

123
def missed_spikes_est(feat, feat_name, spks_per_bin=20, sigma=5, min_num_bins=50, ax=None):
1✔
124
    """
125
    Plots the pdf of an estimated symmetric spike feature distribution, with a vertical cutoff line
126
    that indicates the approximate fraction of spikes missing from the distribution, assuming the
127
    true distribution is symmetric.
128

129
    Parameters
130
    ----------
131
    feat : ndarray
132
        The spikes' feature values.
133
    feat_name : string
134
        The spike feature to plot.
135
    spks_per_bin : int (optional)
136
        The number of spikes per bin from which to compute the spike feature histogram.
137
    sigma : int (optional)
138
        The standard deviation for the gaussian kernel used to compute the pdf from the spike
139
        feature histogram.
140
    min_num_bins : int (optional)
141
        The minimum number of bins used to compute the spike feature histogram.
142
    ax : axessubplot (optional)
143
        The axis handle to plot the histogram on. (if `None`, a new figure and axis is created)
144

145
    Returns
146
    -------
147
    fraction_missing : float
148
        The fraction of missing spikes (0-0.5). *Note: If more than 50% of spikes are missing, an
149
        accurate estimate isn't possible.
150

151
    See Also
152
    --------
153
    single_units.feature_cutoff
154

155
    Examples
156
    --------
157
    1) Plot cutoff line indicating the fraction of spikes missing from a unit based on the recorded
158
    unit's spike amplitudes, assuming the distribution of the unit's spike amplitudes is symmetric.
159
        >>> feat = units_b['amps']['1']
160
        >>> fraction_missing = bb.plot.missed_spikes_est(feat, feat_name='amps', unit=1)
161
    """
162

163
    # Calculate the feature distribution histogram and fraction of spikes missing.
NEW
164
    fraction_missing, pdf, cutoff_idx = single_units.missed_spikes_est(feat, spks_per_bin, sigma, min_num_bins)
×
165

166
    # Plot.
167
    if ax is None:  # create two axes
×
168
        fig, ax = plt.subplots(nrows=1, ncols=2)
×
169
    if ax is None or len(ax) == 2:  # plot histogram and pdf on two separate axes
×
170
        num_bins = int(feat.size / spks_per_bin)
×
171
        ax[0].hist(feat, bins=num_bins)
×
172
        ax[0].set_xlabel('{0}'.format(feat_name))
×
173
        ax[0].set_ylabel('Count')
×
174
        ax[0].set_title('Histogram of {0}'.format(feat_name))
×
175
        ax[1].plot(pdf)
×
176
        ax[1].vlines(cutoff_idx, 0, np.max(pdf), colors='r')
×
UNCOV
177
        ax[1].set_xlabel('Bin Number')
×
178
        ax[1].set_ylabel('Density')
×
NEW
179
        ax[1].set_title('PDF Symmetry Cutoff\n(estimated {:.2f}% missing spikes)'.format(fraction_missing * 100))
×
180
    else:  # just plot pdf
181
        ax = ax[0]
×
182
        ax.plot(pdf)
×
UNCOV
183
        ax.vlines(cutoff_idx, 0, np.max(pdf), colors='r')
×
184
        ax.set_xlabel('Bin Number')
×
UNCOV
185
        ax.set_ylabel('Density')
×
NEW
186
        ax.set_title('PDF Symmetry Cutoff\n(estimated {:.2f}% missing spikes)'.format(fraction_missing * 100))
×
187

UNCOV
188
    return fraction_missing
×
189

190

191
def wf_comp(ephys_file, ts1, ts2, ch, sr=30000, n_ch_probe=385, dtype='int16', car=True, col=['b', 'r'], ax=None):
1✔
192
    """
193
    Plots two different sets of waveforms across specified channels after (optionally)
194
    common-average-referencing. In this way, waveforms can be compared to see if there is,
195
    e.g. drift during the recording, or if two units should be merged, or one unit should be split.
196

197
    Parameters
198
    ----------
199
    ephys_file : string
200
        The file path to the binary ephys data.
201
    ts1 : array_like
202
        A set of timestamps for which to compare waveforms with `ts2`.
203
    ts2: array_like
204
        A set of timestamps for which to compare waveforms with `ts1`.
205
    ch : array-like
206
        The channels to use for extracting and plotting the waveforms.
207
    sr : int (optional)
208
        The sampling rate (in hz) that the ephys data was acquired at.
209
    n_ch_probe : int (optional)
210
        The number of channels of the recording.
211
    dtype: str (optional)
212
        The datatype represented by the bytes in `ephys_file`.
213
    car: bool (optional)
214
        A flag for whether or not to perform common-average-referencing before extracting waveforms
215
    col: list of strings or float arrays (optional)
216
        Two elements in the list, where each specifies the color the `ts1` and `ts2` waveforms
217
        will be plotted in, respectively.
218
    ax : axessubplot (optional)
219
        The axis handle to plot the histogram on. (if `None`, a new figure and axis is created)
220

221
    Returns
222
    -------
223
    wf1 : ndarray
224
        The waveforms for the spikes in `ts1`: an array of shape (#spikes, #samples, #channels).
225
    wf2 : ndarray
226
        The waveforms for the spikes in `ts2`: an array of shape (#spikes, #samples, #channels).
227
    s : float
228
        The similarity score between the two sets of waveforms, calculated by
229
        `single_units.wf_similarity`
230

231
    See Also
232
    --------
233
    io.extract_waveforms
234
    single_units.wf_similarity
235

236
    Examples
237
    --------
238
    1) Compare first and last 100 spike waveforms for unit1, across 20 channels around the channel
239
    of max amplitude, and compare the waveforms in the first minute to the waveforms in the fourth
240
    minutes for unit2, across 10 channels around the mean.
241
        # Get first and last 100 spikes, and 20 channels around channel of max amp for unit 1:
242
        >>> ts1 = units_b['times']['1'][:100]
243
        >>> ts2 = units_b['times']['1'][-100:]
244
        >>> max_ch = clstrs_b['channels'][1]
245
        >>> if max_ch < n_c_ch:  # take only channels greater than `max_ch`.
246
        >>>     ch = np.arange(max_ch, max_ch + 20)
247
        >>> elif (max_ch + n_c_ch) > n_ch_probe:  # take only channels less than `max_ch`.
248
        >>>     ch = np.arange(max_ch - 20, max_ch)
249
        >>> else:  # take `n_c_ch` around `max_ch`.
250
        >>>     ch = np.arange(max_ch - 10, max_ch + 10)
251
        >>> wf1, wf2, s = bb.plot.wf_comp(path_to_ephys_file, ts1, ts2, ch)
252
        # Plot waveforms for unit2 from the first and fourth minutes across 10 channels.
253
        >>> ts = units_b['times']['2']
254
        >>> ts1_2 = ts[np.where(ts<60)[0]]
255
        >>> ts2_2 = ts[np.where(ts>180)[0][:len(ts1)]]
256
        >>> max_ch = clstrs_b['channels'][2]
257
        >>> if max_ch < n_c_ch:  # take only channels greater than `max_ch`.
258
        >>>     ch = np.arange(max_ch, max_ch + 10)
259
        >>> elif (max_ch + n_c_ch) > n_ch_probe:  # take only channels less than `max_ch`.
260
        >>>     ch = np.arange(max_ch - 10, max_ch)
261
        >>> else:  # take `n_c_ch` around `max_ch`.
262
        >>>     ch = np.arange(max_ch - 5, max_ch + 5)
263
        >>> wf1_2, wf2_2, s_2 = bb.plot.wf_comp(path_to_ephys_file, ts1_2, ts2_2, ch)
264
    """
265

266
    # Ensure `ch` is ndarray
267
    ch = np.asarray(ch)
×
UNCOV
268
    ch = ch.reshape((ch.size, 1)) if ch.size == 1 else ch
×
269

270
    # Extract the waveforms for these timestamps and compute similarity score.
NEW
271
    wf1 = extract_waveforms(ephys_file, ts1, ch, sr=sr, n_ch_probe=n_ch_probe, dtype=dtype, car=car)
×
NEW
272
    wf2 = extract_waveforms(ephys_file, ts2, ch, sr=sr, n_ch_probe=n_ch_probe, dtype=dtype, car=car)
×
273
    s = single_units.wf_similarity(wf1, wf2)
×
274

275
    # Plot these waveforms against each other.
276
    n_ch = ch.size
×
277
    if ax is None:
×
278
        fig, ax = plt.subplots(nrows=n_ch, ncols=2)  # left col is all waveforms, right col is mean
×
279
    for cur_ax, cur_ch in enumerate(ch):
×
UNCOV
280
        ax[cur_ax][0].plot(wf1[:, :, cur_ax].T, c=col[0])
×
281
        ax[cur_ax][0].plot(wf2[:, :, cur_ax].T, c=col[1])
×
UNCOV
282
        ax[cur_ax][1].plot(np.mean(wf1[:, :, cur_ax], axis=0), c=col[0])
×
UNCOV
283
        ax[cur_ax][1].plot(np.mean(wf2[:, :, cur_ax], axis=0), c=col[1])
×
UNCOV
284
        ax[cur_ax][0].set_ylabel('Ch {0}'.format(cur_ch))
×
UNCOV
285
    ax[0][0].set_title('All Waveforms. S = {:.2f}'.format(s))
×
UNCOV
286
    ax[0][1].set_title('Mean Waveforms')
×
UNCOV
287
    plt.legend(['1st spike set', '2nd spike set'])
×
288

UNCOV
289
    return wf1, wf2, s
×
290

291

292
def amp_heatmap(ephys_file, ts, ch, sr=30000, n_ch_probe=385, dtype='int16', cmap_name='RdBu', car=True, ax=None):
1✔
293
    """
294
    Plots a heatmap of the normalized voltage values over time and space for given timestamps and
295
    channels, after (optionally) common-average-referencing.
296

297
    Parameters
298
    ----------
299
    ephys_file : string
300
        The file path to the binary ephys data.
301
    ts: array_like
302
        A set of timestamps for which to get the voltage values.
303
    ch : array-like
304
        The channels to use for extracting the voltage values.
305
    sr : int (optional)
306
        The sampling rate (in hz) that the ephys data was acquired at.
307
    n_ch_probe : int (optional)
308
        The number of channels of the recording.
309
    dtype: str (optional)
310
        The datatype represented by the bytes in `ephys_file`.
311
    cmap_name : string (optional)
312
        The name of the colormap associated with the plot.
313
    car: bool (optional)
314
        A flag for whether or not to perform common-average-referencing before extracting waveforms
315
    ax : axessubplot (optional)
316
        The axis handle to plot the histogram on. (if `None`, a new figure and axis is created)
317

318
    Returns
319
    -------
320
    v_vals : ndarray
321
        The voltage values.
322

323
    Examples
324
    --------
325
    1) Plot a heatmap of the spike amplitudes across 20 channels around the channel of max
326
    amplitude for all spikes in unit 1.
327
        >>> ts = units_b['times']['1']
328
        >>> max_ch = clstrs_b['channels'][1]
329
        >>> if max_ch < n_c_ch:  # take only channels greater than `max_ch`.
330
        >>>     ch = np.arange(max_ch, max_ch + 20)
331
        >>> elif (max_ch + n_c_ch) > n_ch_probe:  # take only channels less than `max_ch`.
332
        >>>     ch = np.arange(max_ch - 20, max_ch)
333
        >>> else:  # take `n_c_ch` around `max_ch`.
334
        >>>     ch = np.arange(max_ch - 10, max_ch + 10)
335
        >>> bb.plot.amp_heatmap(path_to_ephys_file, ts, ch)
336
    """
337
    # Ensure `ch` is ndarray
UNCOV
338
    ch = np.asarray(ch)
×
UNCOV
339
    ch = ch.reshape((ch.size, 1)) if ch.size == 1 else ch
×
340

341
    # Get memmapped array of `ephys_file`
342
    s_reader = spikeglx.Reader(ephys_file, open=True)
×
343
    file_m = s_reader.data
×
344

345
    # Get voltage values for each peak amplitude sample for `ch`.
346
    max_amp_samples = (ts * sr).astype(int)
×
347
    # Currently this is an annoying way to calculate `v_vals` b/c indexing with multiple values
348
    # is currently unsupported.
349
    v_vals = np.zeros((max_amp_samples.size, ch.size))
×
350
    for sample in range(max_amp_samples.size):
×
NEW
351
        v_vals[sample] = file_m[max_amp_samples[sample] : max_amp_samples[sample] + 1, ch]
×
UNCOV
352
    if car:  # compute spatial noise in chunks, and subtract from `v_vals`.
×
353
        # Get subset of time (from first to last max amp sample)
354
        n_chunk_samples = 5e6  # number of samples per chunk
×
NEW
355
        n_chunks = np.ceil((max_amp_samples[-1] - max_amp_samples[0]) / n_chunk_samples).astype('int')
×
356
        # Get samples that make up each chunk. e.g. `chunk_sample[1] - chunk_sample[0]` are the
357
        # samples that make up the first chunk.
NEW
358
        chunk_sample = np.arange(max_amp_samples[0], max_amp_samples[-1], n_chunk_samples, dtype=int)
×
UNCOV
359
        chunk_sample = np.append(chunk_sample, max_amp_samples[-1])
×
360
        noise_s_chunks = np.zeros((n_chunks, ch.size), dtype=np.int16)  # spatial noise array
×
361
        # Give time estimate for computing `noise_s_chunks`.
362
        t0 = time.perf_counter()
×
NEW
363
        np.median(file_m[chunk_sample[0] : chunk_sample[1], ch], axis=0)
×
364
        dt = time.perf_counter() - t0
×
NEW
365
        print(
×
366
            'Performing spatial CAR before waveform extraction. Estimated time is {:.2f} mins. ({})'.format(
367
                dt * n_chunks / 60, time.ctime()
368
            )
369
        )
370
        # Compute noise for each chunk, then take the median noise of all chunks.
371
        for chunk in range(n_chunks):
×
NEW
372
            noise_s_chunks[chunk, :] = np.median(file_m[chunk_sample[chunk] : chunk_sample[chunk + 1], ch], axis=0)
×
373
        noise_s = np.median(noise_s_chunks, axis=0)
×
374
        v_vals -= noise_s[None, :]
×
375
        print('Done. ({})'.format(time.ctime()))
×
376
    s_reader.close()
×
377

378
    # Plot heatmap.
379
    if ax is None:
×
380
        fig, ax = plt.subplots()
×
UNCOV
381
    v_vals_norm = (v_vals / np.max(abs(v_vals))).T
×
NEW
382
    cbar_map = ax.imshow(v_vals_norm, cmap=cmap_name, aspect='auto', extent=[ts[0], ts[-1], ch[0], ch[-1]], origin='lower')
×
UNCOV
383
    ax.set_yticks(np.arange(ch[0], ch[-1], 5))
×
UNCOV
384
    ax.set_ylabel('Channel Numbers')
×
UNCOV
385
    ax.set_xlabel('Time (s)')
×
UNCOV
386
    ax.set_title('Voltage Heatmap')
×
UNCOV
387
    fig = ax.figure
×
UNCOV
388
    cbar = fig.colorbar(cbar_map, ax=ax)
×
UNCOV
389
    cbar.set_label('V', rotation=-90)
×
390

UNCOV
391
    return v_vals
×
392

393

394
def firing_rate(ts, hist_win=0.01, fr_win=0.5, n_bins=10, show_fr_cv=True, ax=None):
1✔
395
    """
396
    Plots the instantaneous firing rate of for given spike timestamps over time, and optionally
397
    overlays the value of the coefficient of variation of the firing rate for a specified number
398
    of bins.
399

400
    Parameters
401
    ----------
402
    ts : ndarray
403
        The spike timestamps from which to compute the firing rate.
404
    hist_win : float (optional)
405
        The time window (in s) to use for computing spike counts.
406
    fr_win : float (optional)
407
        The time window (in s) to use as a moving slider to compute the instantaneous firing rate.
408
    n_bins : int (optional)
409
        The number of bins in which to compute coefficients of variation of the firing rate.
410
    show_fr_cv : bool (optional)
411
        A flag for whether or not to compute and show the coefficients of variation of the firing
412
        rate for `n_bins`.
413
    ax : axessubplot (optional)
414
        The axis handle to plot the histogram on. (if `None`, a new figure and axis is created)
415

416
    Returns
417
    -------
418
    fr: ndarray
419
        The instantaneous firing rate over time (in hz).
420
    cv: float
421
        The mean coefficient of variation of the firing rate of the `n_bins` number of coefficients
422
        computed. Can only be returned if `show_fr_cv` is True.
423
    cvs: ndarray
424
        The coefficients of variation of the firing for each bin of `n_bins`. Can only be returned
425
        if `show_fr_cv` is True.
426

427
    See Also
428
    --------
429
    single_units.firing_rate_cv
430
    singecell.firing_rate
431

432
    Examples
433
    --------
434
    1) Plot the firing rate for unit 1 from the time of its first to last spike, showing the cv
435
    of the firing rate for 10 evenly spaced bins.
436
        >>> ts = units_b['times']['1']
437
        >>> fr, cv, cvs = bb.plot.firing_rate(ts)
438
    """
439

440
    if ax is None:
×
UNCOV
441
        fig, ax = plt.subplots()
×
442
    if not (show_fr_cv):  # compute just the firing rate
×
443
        fr = singlecell.firing_rate(ts, hist_win=hist_win, fr_win=fr_win)
×
444
    else:  # compute firing rate and coefficients of variation
NEW
445
        cv, cvs, fr = single_units.firing_rate_coeff_var(ts, hist_win=hist_win, fr_win=fr_win, n_bins=n_bins)
×
446
    x = np.arange(fr.size) * hist_win
×
447
    ax.plot(x, fr)
×
UNCOV
448
    ax.set_title('Firing Rate')
×
449
    ax.set_xlabel('Time (s)')
×
450
    ax.set_ylabel('Rate (s$^-1$)')
×
451

UNCOV
452
    if not (show_fr_cv):
×
UNCOV
453
        return fr
×
454
    else:  # show coefficients of variation
UNCOV
455
        y_max = np.max(fr) * 1.05
×
UNCOV
456
        x_l = x[int(x.size / n_bins)]
×
457
        # Plot vertical lines separating plots into `n_bins`.
NEW
458
        [ax.vlines((x_l * i), 0, y_max, linestyles='dashed', linewidth=2) for i in range(1, n_bins)]
×
459
        # Plot text with cv of firing rate for each bin.
NEW
460
        [ax.text(x_l * (i + 1), y_max, 'cv={0:.2f}'.format(cvs[i]), fontsize=9, ha='right') for i in range(n_bins)]
×
UNCOV
461
        return fr, cv, cvs
×
462

463

464
def peri_event_time_histogram(
1✔
465
    spike_times,
466
    spike_clusters,
467
    events,
468
    cluster_id,  # Everything you need for a basic plot
469
    t_before=0.2,
470
    t_after=0.5,
471
    bin_size=0.025,
472
    smoothing=0.025,
473
    as_rate=True,
474
    include_raster=False,
475
    n_rasters=None,
476
    error_bars='std',
477
    ax=None,
478
    pethline_kwargs={'color': 'blue', 'lw': 2},
479
    errbar_kwargs={'color': 'blue', 'alpha': 0.5},
480
    eventline_kwargs={'color': 'black', 'alpha': 0.5},
481
    raster_kwargs={'color': 'black', 'lw': 0.5},
482
    **kwargs,
483
):
484
    """
485
    Plot peri-event time histograms, with the meaning firing rate of units centered on a given
486
    series of events. Can optionally add a raster underneath the PETH plot of individual spike
487
    trains about the events.
488

489
    Parameters
490
    ----------
491
    spike_times : array_like
492
        Spike times (in seconds)
493
    spike_clusters : array-like
494
        Cluster identities for each element of spikes
495
    events : array-like
496
        Times to align the histogram(s) to
497
    cluster_id : int
498
        Identity of the cluster for which to plot a PETH
499

500
    t_before : float, optional
501
        Time before event to plot (default: 0.2s)
502
    t_after : float, optional
503
        Time after event to plot (default: 0.5s)
504
    bin_size :float, optional
505
        Width of bin for histograms (default: 0.025s)
506
    smoothing : float, optional
507
        Sigma of gaussian smoothing to use in histograms. (default: 0.025s)
508
    as_rate : bool, optional
509
        Whether to use spike counts or rates in the plot (default: `True`, uses rates)
510
    include_raster : bool, optional
511
        Whether to put a raster below the PETH of individual spike trains (default: `False`)
512
    n_rasters : int, optional
513
        If include_raster is True, the number of rasters to include. If `None`
514
        will default to plotting rasters around all provided events. (default: `None`)
515
    error_bars : {'std', 'sem', 'none'}, optional
516
        Defines which type of error bars to plot. Options are:
517
        -- `'std'` for 1 standard deviation
518
        -- `'sem'` for standard error of the mean
519
        -- `'none'` for only plotting the mean value
520
        (default: `'std'`)
521
    ax : matplotlib axes, optional
522
        If passed, the function will plot on the passed axes. Note: current
523
        behavior causes whatever was on the axes to be cleared before plotting!
524
        (default: `None`)
525
    pethline_kwargs : dict, optional
526
        Dict containing line properties to define PETH plot line. Default
527
        is a blue line with weight of 2. Needs to have color. See matplotlib plot documentation
528
        for more options.
529
        (default: `{'color': 'blue', 'lw': 2}`)
530
    errbar_kwargs : dict, optional
531
        Dict containing fill-between properties to define PETH error bars.
532
        Default is a blue fill with 50 percent opacity.. Needs to have color. See matplotlib
533
        fill_between documentation for more options.
534
        (default: `{'color': 'blue', 'alpha': 0.5}`)
535
    eventline_kwargs : dict, optional
536
        Dict containing fill-between properties to define line at event.
537
        Default is a black line with 50 percent opacity.. Needs to have color. See matplotlib
538
        vlines documentation for more options.
539
        (default: `{'color': 'black', 'alpha': 0.5}`)
540
    raster_kwargs : dict, optional
541
        Dict containing properties defining lines in the raster plot.
542
        Default is black lines with line width of 0.5. See matplotlib vlines for more options.
543
        (default: `{'color': 'black', 'lw': 0.5}`)
544

545
    Returns
546
    -------
547
        ax : matplotlib axes
548
            Axes with all of the plots requested.
549
    """
550

551
    # Check to make sure if we fail, we fail in an informative way
552
    if not len(spike_times) == len(spike_clusters):
×
553
        raise ValueError('Spike times and clusters are not of the same shape')
×
554
    if len(events) == 1:
×
555
        raise ValueError('Cannot make a PETH with only one event.')
×
556
    if error_bars not in ('std', 'sem', 'none'):
×
557
        raise ValueError('Invalid error bar type was passed.')
×
558
    if not all(np.isfinite(events)):
×
NEW
559
        raise ValueError(
×
560
            'There are NaN or inf values in the list of events passed.  Please remove non-finite data points and try again.'
561
        )
562

563
    # Compute peths
NEW
564
    peths, binned_spikes = singlecell.calculate_peths(
×
565
        spike_times, spike_clusters, [cluster_id], events, t_before, t_after, bin_size, smoothing, as_rate
566
    )
567
    # Construct an axis object if none passed
568
    if ax is None:
×
569
        plt.figure()
×
570
        ax = plt.gca()
×
571
    # Plot the curve and add error bars
572
    mean = peths.means[0, :]
×
573
    ax.plot(peths.tscale, mean, **pethline_kwargs)
×
574
    if error_bars == 'std':
×
575
        bars = peths.stds[0, :]
×
576
    elif error_bars == 'sem':
×
577
        bars = peths.stds[0, :] / np.sqrt(len(events))
×
578
    else:
579
        bars = np.zeros_like(mean)
×
580
    if error_bars != 'none':
×
581
        ax.fill_between(peths.tscale, mean - bars, mean + bars, **errbar_kwargs)
×
582

583
    # Plot the event marker line. Extends to 5% higher than max value of means plus any error bar.
584
    plot_edge = (mean.max() + bars[mean.argmax()]) * 1.05
×
NEW
585
    ax.vlines(0.0, 0.0, plot_edge, **eventline_kwargs)
×
586
    # Set the limits on the axes to t_before and t_after. Either set the ylim to the 0 and max
587
    # values of the PETH, or if we want to plot a spike raster below, create an equal amount of
588
    # blank space below the zero where the raster will go.
589
    ax.set_xlim([-t_before, t_after])
×
NEW
590
    ax.set_ylim([-plot_edge if include_raster else 0.0, plot_edge])
×
591
    # Put y ticks only at min, max, and zero
592
    if mean.min() != 0:
×
593
        ax.set_yticks([0, mean.min(), mean.max()])
×
594
    else:
NEW
595
        ax.set_yticks([0.0, mean.max()])
×
596
    # Move the x axis line from the bottom of the plotting space to zero if including a raster,
597
    # Then plot the raster
598
    if include_raster:
×
599
        if n_rasters is None:
×
600
            n_rasters = len(events)
×
601
        if n_rasters > 60:
×
NEW
602
            warn('Number of raster traces is greater than 60. This might look bad on the plot.')
×
NEW
603
        ax.axhline(0.0, color='black')
×
604
        tickheight = plot_edge / len(events[:n_rasters])  # How much space per trace
×
NEW
605
        tickedges = np.arange(0.0, -plot_edge - 1e-5, -tickheight)
×
606
        clu_spks = spike_times[spike_clusters == cluster_id]
×
607
        for i, t in enumerate(events[:n_rasters]):
×
608
            idx = np.bitwise_and(clu_spks >= t - t_before, clu_spks <= t + t_after)
×
609
            event_spks = clu_spks[idx]
×
610
            ax.vlines(event_spks - t, tickedges[i + 1], tickedges[i], **raster_kwargs)
×
611
        ax.set_ylabel('Firing Rate' if as_rate else 'Number of spikes', y=0.75)
×
612
    else:
613
        ax.set_ylabel('Firing Rate' if as_rate else 'Number of spikes')
×
614
    ax.spines['top'].set_visible(False)
×
615
    ax.spines['right'].set_visible(False)
×
616
    ax.set_xlabel('Time (s) after event')
×
617
    return ax
×
618

619

620
def driftmap(ts, feat, ax=None, plot_style='bincount', t_bin=0.01, d_bin=20, weights=None, vmax=None, **kwargs):
1✔
621
    """
622
    Plots the values of a spike feature array (y-axis) over time (x-axis).
623
    Two arguments can be given for the plot_style of the drift map:
624
    - 'scatter' : whereby each value is plotted as a marker (up to 100'000 data point)
625
    - 'bincount' : whereby the values are binned (optimised to represent spike raster)
626

627
    Parameters
628
    ----------
629
    feat : ndarray
630
        The spikes' feature values.
631
    ts : ndarray
632
        The spike timestamps from which to compute the firing rate.
633
    ax : axessubplot (optional)
634
        The axis handle to plot the histogram on. (if `None`, a new figure and axis is created)
635
    t_bin: time bin used when plot_style='bincount'
636
    d_bin: depth bin used when plot_style='bincount'
637
    plot_style: 'scatter', 'bincount'
638
    **kwargs: matplotlib.imshow arguments
639

640
    Returns
641
    -------
642
    cd: float
643
        The cumulative drift of `feat`.
644
    md: float
645
        The maximum drift of `feat`.
646

647
    See Also
648
    --------
649
    metrics.cum_drift
650
    metrics.max_drift
651

652
    Examples
653
    --------
654
    1) Plot the amplitude driftmap for unit 1.
655
        >>> ts = units_b['times']['1']
656
        >>> amps = units_b['amps']['1']
657
        >>> ax = bb.plot.driftmap(ts, amps)
658
    2) Plot the depth driftmap for unit 1.
659
        >>> ts = units_b['times']['1']
660
        >>> depths = units_b['depths']['1']
661
        >>> ax = bb.plot.driftmap(ts, depths)
662
    """
663
    iok = ~np.isnan(feat)
1✔
664
    if ax is None:
1✔
UNCOV
665
        fig, ax = plt.subplots()
×
666

667
    if plot_style == 'scatter' and len(ts) < 100000:
1✔
668
        print('here todo')
×
669
        if 'color' not in kwargs.keys():
×
670
            kwargs['color'] = 'k'
×
UNCOV
671
        ax.plot(ts, feat, **kwargs)
×
672
    else:
673
        # compute raster map as a function of site depth
674
        R, times, depths = bincount2D(ts[iok], feat[iok], t_bin, d_bin, weights=weights[iok] if weights is not None else None)
1✔
675
        # plot raster map
676
        ax.imshow(
1✔
677
            R,
678
            aspect='auto',
679
            cmap='binary',
680
            vmin=0,
681
            vmax=vmax or np.std(R) * 4,
682
            extent=np.r_[times[[0, -1]], depths[[0, -1]]],
683
            origin='lower',
684
            **kwargs,
685
        )
686
    ax.set_xlabel('time (secs)')
1✔
687
    ax.set_ylabel('depth (um)')
1✔
688
    return ax
1✔
689

690

691
def pres_ratio(ts, hist_win=10, ax=None):
1✔
692
    """
693
    Plots the presence ratio of spike counts: the number of bins where there is at least one
694
    spike, over the total number of bins, given a specified bin width.
695

696
    Parameters
697
    ----------
698
    ts : ndarray
699
        The spike timestamps from which to compute the presence ratio.
700
    hist_win : float
701
        The time window (in s) to use for computing the presence ratio.
702
    ax : axessubplot (optional)
703
        The axis handle to plot the histogram on. (if `None`, a new figure and axis is created)
704

705
    Returns
706
    -------
707
    pr : float
708
        The presence ratio.
709
    spks_bins : ndarray
710
        The number of spks in each bin.
711

712
    See Also
713
    --------
714
    metrics.pres_ratio
715

716
    Examples
717
    --------
718
    1) Plot the presence ratio for unit 1, given a window of 10 s.
719
        >>> ts = units_b['times']['1']
720
        >>> pr, pr_bins = bb.plot.pres_ratio(ts)
721
    """
722

UNCOV
723
    pr, spks_bins = single_units.pres_ratio(ts, hist_win)
×
UNCOV
724
    pr_bins = np.where(spks_bins > 0, 1, 0)
×
725

UNCOV
726
    if ax is None:
×
UNCOV
727
        fig, ax = plt.subplots()
×
728

729
    ax.plot(pr_bins)
×
730
    ax.set_xlabel('Bin Number (width={:.1f}s)'.format(hist_win))
×
UNCOV
731
    ax.set_ylabel('Presence')
×
732
    ax.set_title('Presence Ratio')
×
733

UNCOV
734
    return pr, spks_bins
×
735

736

737
def driftmap_color(
1✔
738
    clusters_depths, spikes_times, spikes_amps, spikes_depths, spikes_clusters, ax=None, axesoff=False, return_lims=False
739
):
740
    """
741
    Plots the driftmap of a session or a trial
742

743
    The plot shows the spike times vs spike depths.
744
    Each dot is a spike, whose color indicates the cluster
745
    and opacity indicates the spike amplitude.
746

747
    Parameters
748
    -------------
749
    clusters_depths: ndarray
750
        depths of all clusters
751
    spikes_times: ndarray
752
        spike times of all clusters
753
    spikes_amps: ndarray
754
        amplitude of each spike
755
    spikes_depths: ndarray
756
        depth of each spike
757
    spikes_clusters: ndarray
758
        cluster idx of each spike
759
    ax: matplotlib.axes.Axes object (optional)
760
        The axis object to plot the driftmap on
761
        (if `None`, a new figure and axis is created)
762

763
    Return
764
    ---
765
    ax: matplotlib.axes.Axes object
766
        The axis object with driftmap plotted
767
    x_lim: list of two elements
768
        range of x axis
769
    y_lim: list of two elements
770
        range of y axis
771
    """
772

NEW
773
    color_bins = sns.color_palette('hls', 500)
×
NEW
774
    new_color_bins = np.vstack(np.transpose(np.reshape(color_bins, [5, 100, 3]), [1, 0, 2]))
×
775

776
    # get the sorted idx of each depth, and create colors based on the idx
777

UNCOV
778
    sorted_idx = np.argsort(np.argsort(clusters_depths))
×
779

NEW
780
    colors = np.vstack([
×
781
        np.repeat(new_color_bins[np.mod(idx, 500), :][np.newaxis, ...], n_spikes, axis=0)
782
        for (idx, n_spikes) in zip(sorted_idx, np.unique(spikes_clusters, return_counts=True)[1])
783
    ])
784

785
    max_amp = np.percentile(spikes_amps, 90)
×
786
    min_amp = np.percentile(spikes_amps, 10)
×
787
    opacity = np.divide(spikes_amps - min_amp, max_amp - min_amp)
×
788
    opacity[opacity > 1] = 1
×
789
    opacity[opacity < 0] = 0
×
790

791
    colorvec = np.zeros([len(opacity), 4], dtype='float16')
×
792
    colorvec[:, 3] = opacity.astype('float16')
×
793
    colorvec[:, 0:3] = colors.astype('float16')
×
794

795
    x = spikes_times.astype('float32')
×
796
    y = spikes_depths.astype('float32')
×
797

798
    args = dict(color=colorvec, edgecolors='none')
×
799

800
    if ax is None:
×
801
        fig = plt.Figure(dpi=200, frameon=False, figsize=[10, 10])
×
802
        ax = plt.Axes(fig, [0.1, 0.1, 0.9, 0.9])
×
803
        ax.set_xlabel('Time (sec)')
×
804
        ax.set_ylabel('Distance from the probe tip (um)')
×
805
        savefig = True
×
806
        args.update(s=0.1)
×
807

808
    ax.scatter(x, y, **args)
×
809
    x_edge = (max(x) - min(x)) * 0.05
×
810
    x_lim = [min(x) - x_edge, max(x) + x_edge]
×
811
    y_lim = [min(y) - 50, max(y) + 100]
×
812
    ax.set_xlim(x_lim[0], x_lim[1])
×
813
    ax.set_ylim(y_lim[0], y_lim[1])
×
814

815
    if axesoff:
×
816
        ax.axis('off')
×
817

818
    if savefig:
×
819
        fig.add_axes(ax)
×
820
        fig.savefig('driftmap.png')
×
821

822
    if return_lims:
×
823
        return ax, x_lim, y_lim
×
824
    else:
825
        return ax
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc