2. Finding Spikes!

Note

This section primairly focuses using IPFX to analyze your patch clamp data. In particular focuses on analyzing current clamp data. However you may find success with other analysis packages such as https://github.com/BlueBrain/BluePyEfe

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pyabf
import ipfx
from IPython.display import display, HTML
C:\Users\SMest\Anaconda3\lib\site-packages\numpy\_distributor_init.py:32: UserWarning: loaded more than 1 DLL from .libs:
C:\Users\SMest\Anaconda3\lib\site-packages\numpy\.libs\libopenblas.NOIJJG62EMASZI6NYURL6JBKM4EVBGM7.gfortran-win_amd64.dll
C:\Users\SMest\Anaconda3\lib\site-packages\numpy\.libs\libopenblas.PYQHXLVVQ7VESDPUVUADXEVJOBGHJPAY.gfortran-win_amd64.dll
  stacklevel=1)
In C:\Users\SMest\Anaconda3\lib\site-packages\matplotlib\mpl-data\stylelib\_classic_test.mplstyle: 
The text.latex.preview rcparam was deprecated in Matplotlib 3.3 and will be removed two minor releases later.
In C:\Users\SMest\Anaconda3\lib\site-packages\matplotlib\mpl-data\stylelib\_classic_test.mplstyle: 
The mathtext.fallback_to_cm rcparam was deprecated in Matplotlib 3.3 and will be removed two minor releases later.
In C:\Users\SMest\Anaconda3\lib\site-packages\matplotlib\mpl-data\stylelib\_classic_test.mplstyle: Support for setting the 'mathtext.fallback_to_cm' rcParam is deprecated since 3.3 and will be removed two minor releases later; use 'mathtext.fallback : 'cm' instead.
In C:\Users\SMest\Anaconda3\lib\site-packages\matplotlib\mpl-data\stylelib\_classic_test.mplstyle: 
The validate_bool_maybe_none function was deprecated in Matplotlib 3.3 and will be removed two minor releases later.
In C:\Users\SMest\Anaconda3\lib\site-packages\matplotlib\mpl-data\stylelib\_classic_test.mplstyle: 
The savefig.jpeg_quality rcparam was deprecated in Matplotlib 3.3 and will be removed two minor releases later.
In C:\Users\SMest\Anaconda3\lib\site-packages\matplotlib\mpl-data\stylelib\_classic_test.mplstyle: 
The keymap.all_axes rcparam was deprecated in Matplotlib 3.3 and will be removed two minor releases later.
In C:\Users\SMest\Anaconda3\lib\site-packages\matplotlib\mpl-data\stylelib\_classic_test.mplstyle: 
The animation.avconv_path rcparam was deprecated in Matplotlib 3.3 and will be removed two minor releases later.
In C:\Users\SMest\Anaconda3\lib\site-packages\matplotlib\mpl-data\stylelib\_classic_test.mplstyle: 
The animation.avconv_args rcparam was deprecated in Matplotlib 3.3 and will be removed two minor releases later.
abf = pyabf.ABF("data/example1.abf") #this tells pyabf to load our data and store it in an abf object
abf.setSweep(7)

Setting up our spike finder

To begin with, we will import just the spike finder from IPFX.

from ipfx.feature_extractor import SpikeFeatureExtractor

Now we will create our SpikeFeatureExtractor. To do so, we will create a object similar to our ABF object. We can pass our settings to the object, that way we will not have to specify the settings repeatedly. More on this later

spike_fe = SpikeFeatureExtractor(filter=0) #Set the filter to 0

The above command intializes the spike feature extractor. Similar to the abf object, this has many attributes we can access.

dir(spike_fe)[-10:]
['max_interval',
 'min_height',
 'min_peak',
 'process',
 'reject_at_stim_start_interval',
 'spike_feature',
 'spike_feature_keys',
 'spikes',
 'start',
 'thresh_frac']

Getting the Spikes

To begin, we will use the spike.process function to find our data. This function takes our Time, Response, and Command waveforms as input.

spikes = spike_fe.process(abf.sweepX, abf.sweepY, abf.sweepC) #Processes that specific sweep from the ABF file
INFO:numexpr.utils:Note: NumExpr detected 16 cores but "NUMEXPR_MAX_THREADS" not set, so enforcing safe limit of 8.
INFO:numexpr.utils:NumExpr defaulting to 8 threads.

This returns a pandas dataframe containing information for each spike detected. These dataframes are similar to excel spread sheets. Each row is a single spike, and each column is a feature of that spike (for example peak voltage). For more explicit definitions of the various columns I recommend checking out the Allen Institutes Ephys Whitepaper. We can take a peek:

display(spikes)
threshold_index clipped threshold_t threshold_v threshold_i peak_index peak_t peak_v peak_i trough_index ... adp_index adp_t adp_v adp_i slow_trough_index slow_trough_t slow_trough_v slow_trough_i width upstroke_downstroke_ratio
0 12159 False 0.60795 -37.750244 50.0 12171 0.60855 27.832031 50.0 12214 ... NaN NaN NaN NaN 12214.0 0.61070 -55.786133 50.0 0.00065 2.026667
1 12594 False 0.62970 -33.416748 50.0 12609 0.63045 18.920898 50.0 12671 ... NaN NaN NaN NaN 12671.0 0.63355 -50.323486 50.0 0.00100 1.828125
2 12984 False 0.64920 -32.196045 50.0 13003 0.65015 14.862061 50.0 13072 ... NaN NaN NaN NaN 13072.0 0.65360 -47.973633 50.0 0.00135 1.733333
3 13419 False 0.67095 -30.487061 50.0 13441 0.67205 14.953613 50.0 13524 ... NaN NaN NaN NaN 13524.0 0.67620 -46.783447 50.0 0.00165 1.584416
4 13903 False 0.69515 -29.266357 50.0 13928 0.69640 13.366699 50.0 14021 ... NaN NaN NaN NaN 14021.0 0.70105 -47.302246 50.0 0.00185 1.360000
5 14425 False 0.72125 -29.541016 50.0 14452 0.72260 15.319824 50.0 14544 ... NaN NaN NaN NaN 14544.0 0.72720 -47.851562 50.0 0.00185 1.378378
6 15025 False 0.75125 -28.442383 50.0 15053 0.75265 13.885498 50.0 15154 ... NaN NaN NaN NaN 15154.0 0.75770 -48.431396 50.0 0.00205 1.250000
7 15629 False 0.78145 -28.167725 50.0 15653 0.78265 15.869141 50.0 15762 ... NaN NaN NaN NaN 15762.0 0.78810 -47.882080 50.0 0.00185 1.351351
8 16253 False 0.81265 -29.479980 50.0 16279 0.81395 16.082764 50.0 16376 ... NaN NaN NaN NaN 16376.0 0.81880 -47.546387 50.0 0.00190 1.544118
9 16889 False 0.84445 -29.174805 50.0 16916 0.84580 14.495850 50.0 17036 ... NaN NaN NaN NaN 17036.0 0.85180 -47.882080 50.0 0.00205 1.362319
10 17596 False 0.87980 -29.388428 50.0 17624 0.88120 14.190674 50.0 17752 ... NaN NaN NaN NaN 17752.0 0.88760 -46.539307 50.0 0.00205 1.375000
11 18320 False 0.91600 -29.510498 50.0 18348 0.91740 16.571045 50.0 18447 ... NaN NaN NaN NaN 18447.0 0.92235 -47.882080 50.0 0.00195 1.304348
12 19073 False 0.95365 -28.686523 50.0 19099 0.95495 15.869141 50.0 19226 ... NaN NaN NaN NaN 19226.0 0.96130 -47.210693 50.0 0.00200 1.319445
13 19825 False 0.99125 -28.442383 50.0 19855 0.99275 16.021729 50.0 19984 ... NaN NaN NaN NaN 19984.0 0.99920 -48.217773 50.0 0.00210 1.197183
14 20628 False 1.03140 -28.015137 50.0 20657 1.03285 15.686035 50.0 20802 ... NaN NaN NaN NaN 20802.0 1.04010 -47.485352 50.0 0.00210 1.269841
15 21393 False 1.06965 -29.083252 50.0 21421 1.07105 16.784668 50.0 21559 ... NaN NaN NaN NaN 21559.0 1.07795 -48.370361 50.0 0.00210 1.358209
16 22095 False 1.10475 -28.625488 50.0 22120 1.10600 16.906738 50.0 22250 ... NaN NaN NaN NaN 22250.0 1.11250 -46.417236 50.0 0.00200 1.575758
17 22840 False 1.14200 -30.029297 50.0 22870 1.14350 17.242432 50.0 22998 ... NaN NaN NaN NaN 22998.0 1.14990 -49.102783 50.0 0.00210 1.277778
18 23567 False 1.17835 -29.602051 50.0 23596 1.17980 15.441895 50.0 23729 ... NaN NaN NaN NaN 23729.0 1.18645 -47.515869 50.0 0.00215 1.507692
19 24286 False 1.21430 -29.968262 50.0 24317 1.21585 15.594482 50.0 24460 ... NaN NaN NaN NaN 24460.0 1.22300 -47.668457 50.0 0.00210 1.444444
20 24936 False 1.24680 -29.876709 50.0 24966 1.24830 15.747070 50.0 44707 ... NaN NaN NaN NaN NaN NaN NaN NaN 0.00215 1.550000

21 rows × 36 columns

We can also visualize the detected spikes on a plot:

#isolate the columns for plotting
spike_times = spikes['peak_t'] #This isolates the peak_t columm
spike_peak = spikes['peak_v'] #this isolates the peak_v column
sweepX = abf.sweepX
sweepY = abf.sweepY
sweepC = abf.sweepC
#Create a figure
plt.figure(1)
plt.plot(sweepX, sweepY)
plt.scatter(spike_times, spike_peak, c='r', marker='x') #Plots X's at the peak of the action potential
plt.xlabel("Time (s)")
plt.ylabel(abf.sweepLabelY) #
plt.xlim(0.55, 1.5)
(0.55, 1.5)
images/findingspikes_18_1.png

The pandas dataframe contains a number of useful functions for analysis. For example we can take the mean of a column

mean_trough = spikes['fast_trough_v'].mean()
print(f"Found a mean trough of {mean_trough}")
Found a mean trough of -44.91751480102539

In addition we can save the dataframe for opening later:

spikes.to_csv("out.csv") #Outputs the dataframe to a csv for opening in excel
#The "out.csv" specifies the file location/name

Ensuring you captures the spikes

The above example uses the default spike finding options. This is suitable for many applications. However, one may find that this over/under detects spikes in your data. Take for example, the spikes found in one of the latter sweeps:

abf.setSweep(14)
spikes = spike_fe.process(abf.sweepX, abf.sweepY, abf.sweepC)
#isolate the columns for plotting
spike_times = spikes['peak_t'] #This isolates the peak_t columm
spike_peak = spikes['peak_v'] #this isolates the peak_v column
sweepX = abf.sweepX
sweepY = abf.sweepY
sweepC = abf.sweepC
#Create a figure
plt.figure(1)
plt.plot(sweepX, sweepY)
plt.scatter(spike_times, spike_peak, c='r', marker='x') #Plots X's at the peak of the action potential
plt.xlabel("Time (s)")
plt.ylabel(abf.sweepLabelY) #
plt.xlim(0.55, 1.5)
(0.55, 1.5)
images/findingspikes_26_1.png

It still catches most of the spikes, however, it misses one or two spikes at the end of the train that you may want to pick up. To ensure we pick up these spikes, we can adjust a few parameters available to us. We can view the available options here:

help(spike_fe)
Help on SpikeFeatureExtractor in module ipfx.feature_extractor object:

class SpikeFeatureExtractor(builtins.object)
 |  SpikeFeatureExtractor(start=None, end=None, filter=10.0, dv_cutoff=20.0, max_interval=0.005, min_height=2.0, min_peak=-30.0, thresh_frac=0.05, reject_at_stim_start_interval=0)
 |  
 |  Methods defined here:
 |  
 |  __init__(self, start=None, end=None, filter=10.0, dv_cutoff=20.0, max_interval=0.005, min_height=2.0, min_peak=-30.0, thresh_frac=0.05, reject_at_stim_start_interval=0)
 |      Initialize SweepFeatures object.-
 |      
 |      Parameters
 |      ----------
 |      t : ndarray of times (seconds)
 |      v : ndarray of voltages (mV)
 |      i : ndarray of currents (pA)
 |      start : start of time window for feature analysis (optional)
 |      end : end of time window for feature analysis (optional)
 |      filter : cutoff frequency for 4-pole low-pass Bessel filter in kHz (optional, default 10)
 |      dv_cutoff : minimum dV/dt to qualify as a spike in V/s (optional, default 20)
 |      max_interval : maximum acceptable time between start of spike and time of peak in sec (optional, default 0.005)
 |      min_height : minimum acceptable height from threshold to peak in mV (optional, default 2)
 |      min_peak : minimum acceptable absolute peak level in mV (optional, default -30)
 |      thresh_frac : fraction of average upstroke for threshold calculation (optional, default 0.05)
 |      reject_at_stim_start_interval : duration of window after start to reject potential spikes (optional, default 0)
 |  
 |  is_spike_feature_affected_by_clipping(self, key)
 |  
 |  process(self, t, v, i)
 |  
 |  spike_feature(self, spikes_df, key, include_clipped=False, force_exclude_clipped=False)
 |      Get specified feature for every spike.
 |      
 |      Parameters
 |      ----------
 |      key : feature name
 |      include_clipped: return values for every identified spike, even when clipping means they will be incorrect/undefined
 |      
 |      Returns
 |      -------
 |      spike_feature_values : ndarray of features for each spike
 |  
 |  spike_feature_keys(self, spikes_df)
 |      Get list of every available spike feature.
 |  
 |  spikes(self, spikes_df)
 |      Get all features for each spike as a list of records.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  AFFECTED_BY_CLIPPING = ['trough_t', 'trough_v', 'trough_i', 'trough_in...

First, we will take a look at the dv/dt threshold!

dv_cutoff: dV/dT threshold

The dV/dT threshold is the primary method by which ipfx detects spikes. Putative spikes must pass this check prior to any other settings being applied. By default, ipfx looks for points at which the dV/dT crosses 20mV/mS (V/s) (based on the ephys whitepaper noted above). if we examine the dV/dt of our trace, we can see that the smaller spikes do not reach this threshold

dV = np.diff(abf.sweepY) #take the difference of the adjacent points in mV
dT = np.diff(abf.sweepX * 1000) #take the difference of adjacent points in s * 1000 -> ms
dvdt = dV/dT #compute dvdt in mV/ms -> v/s
plt.plot(abf.sweepX[:-1], dvdt)
plt.xlim(0.55, 0.8)
plt.axhline(20, label='20mV/ms', c='r') #plot a horizontal line at 20mV/ms
plt.axhline(10, label='10mV/ms', c='g') #plot a horizontal line at 20mV/ms
plt.legend()
<matplotlib.legend.Legend at 0x2a9e66cfa58>
images/findingspikes_32_1.png

However, it appears that setting a threshold of 10mV/ms should do the trick! Now we will reinitialize the spike finder with the new threshold

spike_fe = SpikeFeatureExtractor(filter=0, dv_cutoff=10) #reintialize with a dv threshold of 10
spikes = spike_fe.process(abf.sweepX, abf.sweepY, abf.sweepC)
#isolate the columns for plotting
spike_times = spikes['peak_t'] #This isolates the peak_t columm
spike_peak = spikes['peak_v'] #this isolates the peak_v column
sweepX = abf.sweepX
sweepY = abf.sweepY
sweepC = abf.sweepC
#Create a figure
plt.figure(1)
plt.plot(sweepX, sweepY)
plt.scatter(spike_times, spike_peak, c='r', marker='x') #Plots X's at the peak of the action potential
plt.xlabel("Time (s)")
plt.ylabel(abf.sweepLabelY) #
plt.xlim(0.55, 1.5)
(0.55, 1.5)
images/findingspikes_35_1.png

Its looking good! However, lets say we decided to exclude the final spike in the train, we can adjust some of the other parameters to do so.

max_interval: Time between threshold and peak

Max interval defines the maximum length of time between the detected threshold and the detected peak. In many cases the default (5ms) should be fine. However, if you have slow or oddly shaped spikes, you may find adjusting this parameter useful. Take for example our threshold to peak time from our last sweep.

#isolate the columns for plotting
spike_times = spikes['peak_t'] #This isolates the peak_t columm
spike_peak = spikes['peak_v'] #this isolates the peak_v column
final_spike = spikes['peak_t'].to_numpy()[-1] #Gets the time of the last peak
final_threshold = spikes['threshold_t'].to_numpy()[-1] #gets the last threshgold
time = final_spike - final_threshold
print(f"Threshold to peak time of {time*1000}ms")
Threshold to peak time of 1.6499999999999293ms
sweepX = abf.sweepX
sweepY = abf.sweepY
sweepC = abf.sweepC
#Create a figure
plt.figure(1)
plt.plot(sweepX, sweepY)
plt.scatter(spike_times, spike_peak, c='r', marker='x') #Plots X's at the peak of the action potential
plt.plot(np.linspace(final_threshold, final_spike, 10), np.full(10, -20), label="threshold to peak")
plt.xlabel("Time (s)")
plt.legend()
plt.ylabel(abf.sweepLabelY) #
plt.xlim(0.72, 0.74)
plt.ylim(-30, 0)
(-30.0, 0.0)
images/findingspikes_40_1.png

By specifying the threshold-to-peak time we can drop this spike

spike_fe = SpikeFeatureExtractor(filter=0, dv_cutoff=10, max_interval=0.00164) #Note that the units are in seconds
spikes = spike_fe.process(abf.sweepX, abf.sweepY, abf.sweepC)
INFO:root:Need to recalculate threshold-peak pair that exceeds maximum allowed interval (0.001640 s)
INFO:root:Need to recalculate threshold-peak pair that exceeds maximum allowed interval (0.001640 s)
INFO:root:Need to recalculate threshold-peak pair that exceeds maximum allowed interval (0.001640 s)
INFO:root:Need to recalculate threshold-peak pair that exceeds maximum allowed interval (0.001640 s)
INFO:root:Need to recalculate threshold-peak pair that exceeds maximum allowed interval (0.001640 s)
INFO:root:Need to recalculate threshold-peak pair that exceeds maximum allowed interval (0.001640 s)
INFO:root:Need to recalculate threshold-peak pair that exceeds maximum allowed interval (0.001640 s)
INFO:root:Could not redetermine threshold-peak pair - dropping that pair
INFO:root:Could not redetermine threshold-peak pair - dropping that pair
INFO:root:Could not redetermine threshold-peak pair - dropping that pair
INFO:root:Could not redetermine threshold-peak pair - dropping that pair
INFO:root:Could not redetermine threshold-peak pair - dropping that pair
INFO:root:Could not redetermine threshold-peak pair - dropping that pair
INFO:root:Could not redetermine threshold-peak pair - dropping that pair
#isolate the columns for plotting
spike_times = spikes['peak_t'] #This isolates the peak_t columm
spike_peak = spikes['peak_v'] #this isolates the peak_v column
sweepX = abf.sweepX
sweepY = abf.sweepY
sweepC = abf.sweepC
#Create a figure
plt.figure(1)
plt.plot(sweepX, sweepY)
plt.scatter(spike_times, spike_peak, c='r', marker='x') #Plots X's at the peak of the action potential
plt.xlabel("Time (s)")
plt.ylabel(abf.sweepLabelY) #
plt.xlim(0.55, 1.5)
(0.55, 1.5)
images/findingspikes_43_1.png

Note that while we dropped that final spike, we lost other spikes! To remedy this, we can try adjusting the minimum height instead!

min_height: the minimum height between threshold and peak

The min height parameter defines the cutoff for the minimum amount of voltage an action potential must swing upward. This is computed as the difference between the peak and threshold. Therefore we will want to enter an absolute (non-negative) value. AP’s with a height below this value will be rejected. This value defaults of 2mV.

spike_fe = SpikeFeatureExtractor(filter=0, dv_cutoff=10, min_height=5) #reintialize with a min height of 5
spikes = spike_fe.process(abf.sweepX, abf.sweepY, abf.sweepC)
#isolate the columns for plotting
spike_times = spikes['peak_t'] #This isolates the peak_t columm
spike_peak = spikes['peak_v'] #this isolates the peak_v column
sweepX = abf.sweepX
sweepY = abf.sweepY
sweepC = abf.sweepC
#Create a figure
plt.figure(1)
plt.plot(sweepX, sweepY)
plt.scatter(spike_times, spike_peak, c='r', marker='x') #Plots X's at the peak of the action potential
plt.xlabel("Time (s)")
plt.ylabel(abf.sweepLabelY) #
plt.xlim(0.55, 1.5)
(0.55, 1.5)
images/findingspikes_48_1.png

We can see that we have successfully excluded the last spike. If we examine closer we can examine the heights of the action potentials

spike_fe = SpikeFeatureExtractor(filter=0, dv_cutoff=10) #reintialize with a min height of 5
spikes = spike_fe.process(abf.sweepX, abf.sweepY, abf.sweepC)
#isolate the columns for plotting
spike_times = spikes['peak_t'] #This isolates the peak_t columm
spike_peak = spikes['peak_v'] #this isolates the peak_v column
final_spike = spikes['peak_v'].to_numpy()[-1] #Gets the time of the last peak
final_threshold = spikes['threshold_v'].to_numpy()[-1] #gets the last threshgold
height = final_spike - final_threshold
sweepX = abf.sweepX
sweepY = abf.sweepY
sweepC = abf.sweepC
#Create a figure
plt.figure(1)
plt.plot(sweepX, sweepY)
plt.scatter(spike_times, spike_peak, c='r', marker='x') #Plots X's at the peak of the action potential
plt.plot(np.full(10, spike_times.to_numpy()[-1]),np.linspace(final_threshold, final_spike, 10), label="threshold to peak")
plt.xlabel("Time (s)")
plt.legend
plt.ylabel(abf.sweepLabelY) #
plt.xlim(0.72, 0.74)
plt.ylim(-30, 0)
(-30.0, 0.0)
images/findingspikes_52_1.png

min_peak: the minimum absolute peak of the action potential

Similar to the setting above, we can set the cutoff for the min peak of the action potential. However, this value is not calculated in reference to the threshold, instead, it is computed as the minimum the peak must reach (regardless of distance between peak and threshold). Therefore, its okay to use negative numbers for this setting. Action potentials with peaks that do not reach this level will be rejected. This value defaults to -30

spike_fe = SpikeFeatureExtractor(filter=0, dv_cutoff=10, min_peak=-10) #reintialize with a min peak 10
spikes = spike_fe.process(abf.sweepX, abf.sweepY, abf.sweepC)
#isolate the columns for plotting
spike_times = spikes['peak_t'] #This isolates the peak_t columm
spike_peak = spikes['peak_v'] #this isolates the peak_v column
sweepX = abf.sweepX
sweepY = abf.sweepY
sweepC = abf.sweepC
#Create a figure
plt.figure(1)
plt.plot(sweepX, sweepY)
plt.scatter(spike_times, spike_peak, c='r', marker='x') #Plots X's at the peak of the action potential
plt.xlabel("Time (s)")
plt.axhline(-10, label="min_peak threshold of -10")
plt.ylabel(abf.sweepLabelY) 
plt.xlim(0.55, 1.5)
(0.55, 1.5)
images/findingspikes_56_1.png

zooming a bit closer shows us that this threshold of -10 allows us to exclude the last spike but keep the others!

Putting it all together: Find spikes across all sweeps

So far we have just been analyzing a single sweep of our ABF file. However functionally