Spike Sorting with SpikeInterface

Authors
Dr. Ole Bialas | Dr. Nicholas Del Grosso

Setup

Import Libraries

Import the modules required for this notebook

import numpy as np
from matplotlib import pyplot as plt
import spikeinterface.full as si
import probeinterface as pi
from probeinterface.plotting import plot_probe
import scipy

%matplotlib inline

Download Data

Download the Data required for this notebook

import os
import requests
import zipfile

url = "https://uni-bonn.sciebo.de/s/qrXWd4ZHozGdJwN"
response = requests.get(url + "/download")
print("Downloading Data ...")
with open("openephys_recording.zip", "wb") as file:
    file.write(response.content)

with zipfile.ZipFile("openephys_recording.zip", "r") as zip_ref:
    zip_ref.extractall("")
os.remove("openephys_recording.zip")
print("Done!")

Section 1: Working with Recordings

SpikeInterface provides a unified interface for reading, handling, and processing extracellular electrophysiological recordings from a wide variety of file formats. In this section, you will learn how to load recordings, inspect their properties, and extract data segments for further analysis. This is the first step in any spike sorting workflow, as it allows you to access and manipulate the raw voltage traces recorded from your experiment.

Code Description
rec = si.read_openephys(data_dir, stream_id="1") Read a specific stream from the Open Ephys recording in data_dir.
rec.get_num_channels() Get the number of channels in the recording rec.
rec.get_duration() Get the total duration (in seconds) of the recording rec.
rec.get_sampling_frequency() Get the sampling frequency (in Hz) of the recording rec.
rec.get_traces(start_frame=i, stop_frame=j) Get a NumPy array containing the data recorded between samples i and j.
rec.frame_slice(start_frame, end_frame) Get a segment of the recording between the specified frames.
plt.plot(traces[:, :n]) Plot the traces for the first n channels.

Run the cell below to define the data directory

Exercises

data_dir = "openephys_raw/2023-08-23_15-56-05"

Example: Load the recording stream with the id "1", which contains the auxiliary channels, assign the returned RecordingExtractor to a variable called aux_rec and print it.

rec = si.read_openephys(data_dir, stream_id="1")
rec
OpenEphysBinaryRecordingExtractor: 8 channels - 30.0kHz - 1 segments - 9,000,000 samples - 300.00s (5.00 minutes) - int16 dtype - 137.33 MiB
Channel IDs
    ['ADC1' 'ADC2' 'ADC3' 'ADC4' 'ADC5' 'ADC6' 'ADC7' 'ADC8']
Annotations
  • is_filtered : False
  • experiment_name : experiment1
Properties
    gain_to_uV[152.58789062 152.58789062 152.58789062 152.58789062 152.58789062 152.58789062 152.58789062 152.58789062]
    offset_to_uV[0. 0. 0. 0. 0. 0. 0. 0.]
    physical_unit['V' 'V' 'V' 'V' 'V' 'V' 'V' 'V']
    gain_to_physical_unit[0.00015259 0.00015259 0.00015259 0.00015259 0.00015259 0.00015259 0.00015259 0.00015259]
    offset_to_physical_unit[0. 0. 0. 0. 0. 0. 0. 0.]
    channel_name['ADC1' 'ADC2' 'ADC3' 'ADC4' 'ADC5' 'ADC6' 'ADC7' 'ADC8']

Exercise: Load the stream with the id "0" and assign the returned RecordingExtractor to a variable called rec.

Solution
rec = si.read_openephys(data_dir, stream_id="0")
rec
OpenEphysBinaryRecordingExtractor: 64 channels - 30.0kHz - 1 segments - 9,000,000 samples - 300.00s (5.00 minutes) - int16 dtype - 1.07 GiB
Channel IDs
    ['CH1' 'CH2' 'CH3' 'CH4' 'CH5' 'CH6' 'CH7' 'CH8' 'CH9' 'CH10' 'CH11' 'CH12' 'CH13' 'CH14' 'CH15' 'CH16' 'CH17' 'CH18' 'CH19' 'CH20' 'CH21' 'CH22' 'CH23' 'CH24' 'CH25' 'CH26' 'CH27' 'CH28' 'CH29' 'CH30' 'CH31' 'CH32' 'CH33' 'CH34' 'CH35' 'CH36' 'CH37' 'CH38' 'CH39' 'CH40' 'CH41' 'CH42' 'CH43' 'CH44' 'CH45' 'CH46' 'CH47' 'CH48' 'CH49' 'CH50' 'CH51' 'CH52' 'CH53' 'CH54' 'CH55' 'CH56' 'CH57' 'CH58' 'CH59' 'CH60' 'CH61' 'CH62' 'CH63' 'CH64']
Annotations
  • is_filtered : False
  • experiment_name : experiment1
Properties
    gain_to_uV[0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999]
    offset_to_uV[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
    physical_unit['uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV' 'uV']
    gain_to_physical_unit[0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999 0.19499999]
    offset_to_physical_unit[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
    channel_name['CH1' 'CH2' 'CH3' 'CH4' 'CH5' 'CH6' 'CH7' 'CH8' 'CH9' 'CH10' 'CH11' 'CH12' 'CH13' 'CH14' 'CH15' 'CH16' 'CH17' 'CH18' 'CH19' 'CH20' 'CH21' 'CH22' 'CH23' 'CH24' 'CH25' 'CH26' 'CH27' 'CH28' 'CH29' 'CH30' 'CH31' 'CH32' 'CH33' 'CH34' 'CH35' 'CH36' 'CH37' 'CH38' 'CH39' 'CH40' 'CH41' 'CH42' 'CH43' 'CH44' 'CH45' 'CH46' 'CH47' 'CH48' 'CH49' 'CH50' 'CH51' 'CH52' 'CH53' 'CH54' 'CH55' 'CH56' 'CH57' 'CH58' 'CH59' 'CH60' 'CH61' 'CH62' 'CH63' 'CH64']

Exercise: Get the duration of the recording

Solution
rec.get_duration()
300.0

Exercise: Get the sampling frequency of the recording and assign it to a variable fs

Solution
fs = rec.get_sampling_frequency()

Example: Use the frame_slice method to select the first 10 seconds of the recording rec. Assign the returned object to a new variable seg and get it’s duration.

seg = rec.frame_slice(start_frame=0 * fs, end_frame=10 * fs)
seg.get_duration()
10.000000000000027

Exercise: Use the frame_slice method to select the segment between 0.5 and 0.7 seconds. Assign the returned object to a new variable seg and get it’s duration.

Solution
seg = rec.frame_slice(start_frame=0.5 * fs, end_frame=0.7 * fs)
seg.get_duration()
0.20000000000001594

Exercise: Use seg.get_traces() to get the raw voltage traces and assign them to a variable traces.

Solution
traces = seg.get_traces()

Example: Plot the extracted traces for the first 3 channels

plt.plot(traces[:, :3])

Exercise: Plot the extracted traces for the last 5 channels

Solution
plt.plot(traces[:, -5:])

Section 2: Handling Probe Information

Accurate probe information is essential for interpreting extracellular recordings, as it defines the spatial arrangement and wiring of the electrodes. ProbeInterface, integrated with SpikeInterface, allows you to load, visualize, and configure probe layouts from a wide range of manufacturers. In this section, you will learn how to select a probe, visualize its geometry, remap its wiring, and attach it to your recording for downstream analysis.

Code Description
probe = pi.get_probe("imec", probe="NP2014") Get the "NP2014" probe from "imec" and assign it to probe.
probe = pi.get_probe(manufacturer, probe_name) Get a probe by specifying the manufacturer and probe name.
pi.get_available_pathways() List all available wiring pathways for connecting probes and acquisition systems.
probe.wiring_to_device(pathway) Remap the probe’s channels for a given wiring pathway.
plot_probe(probe, ax=ax) Plot the probe and its channels using a matplotlib axis ax.
plot_probe(probe, ax=ax, with_contact_id=True, with_device_index=True) Plot the probe with contact and device indices.
rec.set_probe(probe) Add the probe information to the recording object rec.
probe = rec.get_probe() Retrieve the probe information from a recording object.
fig, ax = plt.subplots() Create a new matplotlib figure and axis for plotting.
ax.set(xlim=(a, b), ylim=(c, d)) Set the x and y axis limits for the plot.

Exercises

Example: Get the "NP1010" probe from "imec".

manufacturer = "imec"
probe_name = "NP1010"
probe = pi.get_probe(manufacturer, probe_name)
probe
Probe - imec - NP1010 - 960ch - 1shanks

Example: Plot the probe and adjust the axis limits to display the probe’s tip

fig, ax = plt.subplots()
plot_probe(probe, ax=ax)
ax.set(xlim=(-50, 100), ylim=(-200, 150))
[(-50.0, 100.0), (-200.0, 150.0)]

Exercise: Get the "1S256" probe from "plexon"

Solution
manufacturer = 'plexon'
probe_name = '1S256'
probe = pi.get_probe(manufacturer, probe_name)
probe
Probe - Plexon-SiNAPS - 1S256 - 256ch - 1shanks

Exercise: Plot the probe and adjust the axis limits to display the probe’s tip

Solution
fig, ax = plt.subplots()
plot_probe(probe, ax=ax)
ax.set(xlim=(-50, 75), ylim=(-150, 150))
[(-50.0, 75.0), (-150.0, 150.0)]

Exercise: Go to the ProbeInterface library and select a new manufacturer and probe from there. Then, get the probe and plot it

Solution
probe = pi.get_probe("neuronexus", "A4x8-5mm-100-200-177")
fig, ax = plt.subplots()
plot_probe(probe, ax=ax)
ax.set(xlim=(-50, 50), ylim=(-80, 150))
[(-50.0, 50.0), (-80.0, 150.0)]

Example: Load the "ASSY-236-H5" probe from "cambridgeneurotech" and remap the channels for the "cambridgeneurotech_mini-amp-64" wiring pathway. Then, plot the probe with the contact and device indices and adjust the axis limit to view the tip of the probe.

probe = pi.get_probe("cambridgeneurotech", "ASSY-236-H5")
probe.wiring_to_device("cambridgeneurotech_mini-amp-64")

fig, ax = plt.subplots()
plot_probe(probe, ax=ax, with_contact_id=True, with_device_index=True)
ax.set(xlim=(-25, 50), ylim=(-50, 150))
[(-25.0, 50.0), (-50.0, 150.0)]

Exercise: Load the "ASSY-236-H5" probe from "cambridgeneurotech" and remap the channels for the "ASSY-156>RHD2164" wiring pathway. Then, plot the probe with the contact and device indices and adjust the axis limit to view the tip of the probe.

Solution
probe = pi.get_probe("cambridgeneurotech", "ASSY-236-H5")
probe.wiring_to_device("ASSY-156>RHD2164")

fig, ax = plt.subplots()
plot_probe(probe, ax=ax, with_contact_id=True, with_device_index=True)
ax.set(xlim=(-25, 50), ylim=(-50, 150))
[(-25.0, 50.0), (-50.0, 150.0)]

Exercise: List all of the available wiring pathways. Select one and apply it to the "ASSY-236-H5" probe from "cambridgeneurotech" and plot the probe (NOTE: not all wiring pathways are compatible with every device).

Solution
pi.get_available_pathways()
probe = pi.get_probe("cambridgeneurotech", "ASSY-236-H5")
probe.wiring_to_device("ASSY-77>Adpt.A64-Om32_2x-sm-NN>two_RHD2132")

fig, ax = plt.subplots()
plot_probe(probe, ax=ax, with_contact_id=True, with_device_index=True)
ax.set(xlim=(-25, 50), ylim=(-50, 150))
[(-25.0, 50.0), (-50.0, 150.0)]

Exercise: Add the probe to the recording rec using .set_probe() and then call .get_probe() method to verify the probe info was added to the recording.

Solution
rec = rec.set_probe(probe)
rec.get_probe()
Probe - 64ch - 1shanks

Section 3: Spike Sorting

Spike sorting is the process of detecting and classifying action potentials (spikes) from extracellular recordings, assigning them to putative single units (neurons). SpikeInterface provides a unified API to run a variety of spike sorting algorithms (sorters) and manage their outputs. In this section, you will learn how to list available sorters, run a sorter on your data, and inspect the results, including the number of detected units and their spike trains.

Code Description
si.installed_sorters() List all spike sorters installed in the current environment.
si.run_sorter(sorter_name, recording, folder, **kwargs) Run a spike sorter on the given recording, saving results to the specified folder.
results.get_num_units() Get the number of units detected in the spike sorting results.
results.get_unit_ids() Get the list of unit IDs from the sorting results.
results.get_unit_spike_train(unit_id, return_times=True) Get the spike train for a specific unit, optionally returning spike times.
rec.frame_slice(start_frame, end_frame) Select a segment of the recording for spike sorting.
fs = rec.get_sampling_frequency() Get the sampling frequency of the recording.

Run the code below to load the recording, and add the probe information to it.

Exercises

rec = si.read_openephys(data_dir, stream_id="0")
probe = pi.get_probe("cambridgeneurotech", "ASSY-236-H5")
probe.wiring_to_device("cambridgeneurotech_mini-amp-64")
rec = rec.set_probe(probe)

Exercise: List all of the installed spike sorters

Solution
si.installed_sorters()
['simple', 'spykingcircus2', 'tridesclous2']

Example: Select the first 5 seconds of the recording rec and run the spike sorter "tridesclous2" on the recording segment seg.

t_start = 0
t_stop = 5
fs = rec.get_sampling_frequency()
seg = rec.frame_slice(start_frame=t_start * fs, end_frame=t_stop * fs)
sorting_TDC = si.run_sorter(
    sorter_name="tridesclous2",
    recording=seg,
    folder="results_TDC",
    remove_existing_folder=True,
)

Exercise: Get the number of units in sorting_TDC.

Solution
sorting_TDC.get_num_units()
29

Exercise: Get the unit ids in sorting_TDC.

Solution
sorting_TDC.get_unit_ids()
array([ 3,  8,  9, 10, 11, 12, 14, 16, 18, 19, 20, 24, 25, 30, 33, 39, 40,
       41, 43, 44, 45, 47, 48, 49, 52, 53, 65, 67, 69])

Exercise: Get the spike train for any of the units.

Solution
sorting_TDC.get_unit_spike_train(10, return_times=True)
array([353.0968    , 353.1337    , 353.6497    , 353.67243333,
       353.6785    , 353.77126667, 353.78456667, 353.78456667,
       354.0273    , 354.62606667, 354.65626667, 354.65626667,
       354.66563333, 354.68123333, 354.70546667, 354.74256667,
       355.12943333, 355.13546667, 355.14583333, 355.1731    ,
       355.2428    , 355.27963333, 355.5034    , 355.5179    ,
       355.52286667, 355.54753333, 355.577     , 355.61273333,
       356.28896667, 356.37073333, 356.66496667, 356.67043333,
       356.67443333, 356.8355    , 357.4416    , 357.4605    ])

Exercise: Select the first 5 seconds of the recording rec and run the spike sorter "spykingcircus2" with apply_motion_correction=False on the recording segment seg. Then, print the number of identified units.

Solution
t_start = 0
t_stop = 5
fs = rec.get_sampling_frequency()
seg = rec.frame_slice(start_frame=t_start * fs, end_frame=t_stop * fs)
sorting_SPC = si.run_sorter(
    sorter_name="spykingcircus2",
    recording=seg,
    folder="results_TDC",
    remove_existing_folder=True,
    apply_motion_correction=False,
)
sorting_SPC.get_num_units()
spykingcircus2 could benefit from using torch. Consider installing it
55

Section 4: Setting Sorting Parameters

Many spike sorters offer a range of parameters that can be tuned to optimize detection and clustering for your specific dataset. SpikeInterface allows you to query and set these parameters in a standardized way. In this section, you will learn how to retrieve default parameters for a sorter, modify detection and filtering settings, and run sorters with custom configurations to improve sorting performance.

Code Description
si.get_default_sorter_params(sorter_name) Get the default parameters for a given spike sorter.
si.run_sorter(sorter_name, recording, folder, detection=..., filtering=..., **kwargs) Run a spike sorter with custom detection or filtering parameters.
detection = {"detect_threshold": value} Set the detection threshold parameter for a sorter.
filtering = {"freq_min": fmin, "freq_max": fmax} Set the minimum and maximum filter frequencies for a sorter.
svd = {"n_components": 3} Set the number of SVD/PCA components to use for feature extraction.
results.get_num_units() Get the number of detected units after sorting.

Exercise: Get the default parameters for the spike sorter "tridesclous2"

Solution
si.get_default_sorter_params("tridesclous2")
{'apply_preprocessing': True,
 'apply_motion_correction': False,
 'motion_correction': {'preset': 'dredge_fast'},
 'cache_preprocessing': {'mode': 'memory',
  'memory_limit': 0.5,
  'delete_cache': True},
 'waveforms': {'ms_before': 0.5, 'ms_after': 1.5, 'radius_um': 120.0},
 'filtering': {'freq_min': 150.0,
  'freq_max': 5000.0,
  'ftype': 'bessel',
  'filter_order': 2},
 'detection': {'peak_sign': 'neg',
  'detect_threshold': 5,
  'exclude_sweep_ms': 1.5,
  'radius_um': 150.0},
 'selection': {'n_peaks_per_channel': 5000, 'min_n_peaks': 20000},
 'svd': {'n_components': 4},
 'clustering': {'recursive_depth': 5,
  'split_radius_um': 40.0,
  'clusterer': 'isosplit6',
  'clusterer_kwargs': {},
  'do_merge': True,
  'merge_kwargs': {'similarity_metric': 'l1',
   'num_shifts': 4,
   'similarity_thresh': 0.75}},
 'templates': {'ms_before': 2.0,
  'ms_after': 3.0,
  'max_spikes_per_unit': 400,
  'sparsity_threshold': 1.5,
  'min_snr': 2.5},
 'matching': {'method': 'tdc-peeler',
  'method_kwargs': {},
  'gather_mode': 'memory'},
 'job_kwargs': {},
 'save_array': True}

Example: Run the spike sorter "tridesclous2" on the first 5 seconds of the recording rec and set the detection parameter to {"detect_threshold":8}. Then, print the number of detected units.

t_start = 0
t_stop = 5
fs = rec.get_sampling_frequency()
seg = rec.frame_slice(start_frame=t_start * fs, end_frame=t_stop * fs)

detection = {"detect_threshold": 8}
sorting_TDC = si.run_sorter(
    sorter_name="tridesclous2",
    recording=seg,
    folder="results_TDC",
    remove_existing_folder=True,
    detection=detection,
)
sorting_TDC.get_num_units()
29

Exercise: Run the spike sorter "tridesclous2" on the first 10 seconds of the recording rec and set the filtering parameter to {"freq_min": 1000, "freq_max":9000}. Then, print the number of detected units.

Solution
t_start = 0
t_stop = 5
fs = rec.get_sampling_frequency()
seg = rec.frame_slice(start_frame=t_start * fs, end_frame=t_stop * fs)

filtering = {"freq_min": 1000, "freq_max": 9000}
sorting_TDC = si.run_sorter(
    sorter_name="tridesclous2",
    recording=seg,
    folder="results_TDC",
    remove_existing_folder=True,
    filtering=filtering,
)
sorting_TDC.get_num_units()
18

Exercise: Run the spike sorter "tridesclous2" on the segment seg with svd={"n_components":3}

Solution
sorting_TDC = si.run_sorter(
    sorter_name="tridesclous2",
    recording=seg,
    folder="results_TDC",
    remove_existing_folder=True,
    svd={"n_components": 3},
)
sorting_TDC.get_num_units()
27