CSD Analysis Theory and Methods

Authors
Dr. Atle Rimehaug | Dr. Nicholas Del Grosso

The LFP can be challenging to interpret in terms of the underlying neurophysiology. One method that helps make the LFP more interpretable is current source density (CSD) analysis. The CSD is calculated from the LFP and shows where the current sinks and sources generating the LFP are located in space and when they arise. A current sink shows where positive ions enter a neuron/population of neurons, while a current source shows where positive ions leave a neuron/population of neurons.

When a neuron receives excitatory input, positive ions flush into the neuron at the synapse. This will show up as a current sink in a CSD plot. Therefore, a current sink can tell you where and when a population of neurons gets input. There are multiple different neuronal processes that can give rise to a current sink or a current source, so the interpretation is still not always straightforward, but the CSD at least brings us one step closer to interpreting the electrical signals we record in terms of the neurophysiology.

Setup

Import Libraries

import numpy as np
import neo
import quantities as pq
from scipy.stats import pearsonr
import matplotlib.pyplot as plt
from matplotlib.colors import TwoSlopeNorm
from elephant.current_source_density import estimate_csd

Utility Functions

Run the cell below, but feel free to ignore this code — it’s just some utility functions we’ll use later for convenience.

def add_um_units(channels_depth_vals):
    """Add units to coordinates of channels on probe"""
    return np.array([np.abs(channels_depth_vals)]).T*pq.um

def create_neo_lfp(lfp_values, sampling_frequency):
    """Put lfp data in time window in neo format"""
    return neo.AnalogSignal(
        lfp_values.values.T,    # The lfp must be transposed to have time on the first dimension for neo
        units=pq.V, 
        sampling_rate=sampling_frequency*pq.Hz
    )

def get_data_time_window(data, time, time_window_start, time_window_end, is_trial_avg = True):
    """Get lfp or csd data in selected time window"""
    mask_time_window = (time > time_window_start) & (time < time_window_end)

    # get data in time window
    if is_trial_avg:
        data_window = data[:, mask_time_window]
    else:
        data_window = data[:, :, mask_time_window]

    return data_window

def correlate_pairwise_trials(csd):
    '''Correlate csd from all trials pairwise'''
    corr_csd_all_trials = []

    for itrialA in range(csd.shape[1]):
        corr_csd_temp = []
        for itrialB in range(csd.shape[1]):
            corr_csd_temp.append(pearsonr(csd[:,itrialA].flatten(), csd[:,itrialB].flatten())[0])
        corr_csd_all_trials.append(corr_csd_temp)

    return np.array(corr_csd_all_trials)

def euclidian_norm_pairwise_trials(csd):
    '''Correlate csd from all trials pairwise'''
    eucnorm_csd_all_trials = []

    for itrialA in range(csd.shape[1]):
        eucnorm_csd_temp = []
        for itrialB in range(csd.shape[1]):
            eucnorm_csd_temp.append(np.linalg.norm(csd[:,itrialA]-csd[:,itrialB]))
        eucnorm_csd_all_trials.append(eucnorm_csd_temp)

    return np.array(eucnorm_csd_all_trials)


def plot_csd(csd):
    plt.figure()
    plt.imshow(
        np.array(csd.T),                          # The csd must be transposed to get channels on first dimension for the plot
        aspect = 'auto', 
        cmap = 'RdBu_r',  # 
        norm=TwoSlopeNorm(vcenter=0),
    )  # Puts white a the zero value, so blue is always negative and red is always positive
    plt.ylabel('Channel nr.')
    plt.colorbar();


def calc_csd(lfp, sampling_freq, channel_depths, method='StandardCSD', **csd_kwargs) -> 'neo':
    lfp_neo = create_neo_lfp(lfp, sampling_frequency=sampling_freq) # The lfp must be transposed to have time on the first dimension for neo
    coords = add_um_units(channels_depth_vals=channel_depths.values)  # create array with channel positions and units
    csd_neo = estimate_csd(lfp_neo, coordinates=coords, method=method, **csd_kwargs)
    return csd_neo

class utils:
    correlate_pairwise_trials = correlate_pairwise_trials
    euclidian_norm_pairwise_trials = euclidian_norm_pairwise_trials
    calc_csd = calc_csd
    plot_csd = plot_csd

Download dataset

import owncloud

owncloud.Client.from_public_link('https://uni-bonn.sciebo.de/s/rY4YCPXmkpespKG').get_file('/', 'dataset_session_754312389.nc')
True

Section 1: Calculate the Current Source Density (CSD) from the Recorded LFP Using the “Standard” Method.

In this section, you will learn how to calculate the CSD from the LFP using the so-called standard method.

Code Description
utils.calc_csd(lfp, sampling_freq, channel_depths) Calculates a CSD and returns a Neo AnalogSignal
utils.plot_csd(csd) Plots the CSD
pq.um Defines the quantities unit micrometer.
lfp.sel(trial_nr = 0) Gets the data from the first trial.
lfp.mean(dim='trial_nr) Calculate the trial averaged lfp.
gaussian_filter(data, sigma = (filter_width_1st_dim, filter_width_2nd_dim)) Apply a 2D gaussian filter to smooth data. sigma = (filter_width_1st_dim, filter_width_2nd_dim) sets the width of the filter along each axis of the data.

Load Dataset

Run the cell below to load dataset and extract LFP data.

Exercises

import xarray as xr
from pathlib import Path

dataset_path = Path('dataset_session_754312389.nc')

dataset = xr.load_dataset(dataset_path)
dataset = dataset[[
    'lfp_V1',
    'csd_V1'
]]
dataset
<xarray.Dataset> Size: 52MB
Dimensions:        (channel_depth: 23, trial_nr: 75, time_lfp: 1875,
time_csd: 1875)
Coordinates:

  • channel_depth (channel_depth) int64 184B 0 -40 -80 -120 … -800 -840 -880

  • trial_nr (trial_nr) int32 300B 0 1 2 3 4 5 6 … 68 69 70 71 72 73 74

  • time_lfp (time_lfp) float64 15kB -1e+03 -999.2 -998.4 … 498.4 499.2

  • time_csd (time_csd) float64 15kB -1e+03 -999.2 -998.4 … 498.4 499.2 Data variables: lfp_V1 (channel_depth, trial_nr, time_lfp) float64 26MB -8.516e-0… csd_V1 (channel_depth, trial_nr, time_csd) float64 26MB -0.04729 … Attributes: time_unit: millisecond lfp_unit: Volt channel_depth_unit: micrometer note_channel_depth: Measured in distance from electrode closest t… sampling_frequency_lfp: 1250 sampling_frequency_spikes: 1000 sampling_frequency_unit: Hz stimulus_onset: 1000 stimulus_offset: 1250

xarray.Dataset
    • channel_depth: 23
    • trial_nr: 75
    • time_lfp: 1875
    • time_csd: 1875
    • channel_depth
      (channel_depth)
      int64
      0 -40 -80 -120 … -800 -840 -880
      array([   0,  -40,  -80, -120, -160, -200, -240, -280, -320, -360, -400, -440,
      -480, -520, -560, -600, -640, -680, -720, -760, -800, -840, -880])
    • trial_nr
      (trial_nr)
      int32
      0 1 2 3 4 5 6 … 69 70 71 72 73 74
      array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17,
      18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
      36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53,
      54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
      72, 73, 74], dtype=int32)
    • time_lfp
      (time_lfp)
      float64
      -1e+03 -999.2 … 498.4 499.2
      array([-1000. ,  -999.2,  -998.4, …,   497.6,   498.4,   499.2])
    • time_csd
      (time_csd)
      float64
      -1e+03 -999.2 … 498.4 499.2
      array([-1000. ,  -999.2,  -998.4, …,   497.6,   498.4,   499.2])
    • lfp_V1
      (channel_depth, trial_nr, time_lfp)
      float64
      -8.516e-06 -1.156e-05 … 6.131e-06
      array([[[-8.51569421e-06, -1.15631923e-05, -3.76924291e-07, …,
      -2.17654367e-06,  1.68119712e-07, -6.94200500e-06],
      [ 2.81771330e-08,  6.29403676e-06,  9.98029865e-06, …,
      1.01304291e-05,  5.93295341e-06,  1.72969475e-05],
      [-1.27271427e-06, -3.68783112e-06, -4.46340366e-06, …,
      3.65666426e-06,  2.96670978e-06,  1.24568597e-06],
      …,
      [ 4.70565253e-06,  1.94317598e-06,  1.20455675e-06, …,
      4.07503496e-06,  7.65054254e-06,  3.91673831e-06],
      [ 8.38144661e-07,  2.28123812e-06,  3.70896321e-06, …,
      1.34013793e-05,  1.15462596e-05,  8.42411093e-06],
      [ 7.32479652e-06,  1.07711181e-06,  7.00885884e-06, …,
      1.92065863e-05,  1.92065863e-05,  1.92065863e-05]],

      [[ 3.91952787e-06, 1.70056905e-06, 5.97275690e-06, …, 4.06974274e-06, 4.04094749e-06, -4.68055730e-06], [-4.10311367e-06, 5.00856269e-06, 6.20500275e-06, …, 8.75868311e-06, 5.69604080e-06, 8.57744575e-06], [ 1.09646506e-06, 1.62070262e-06, 1.10953040e-07, …, 1.01990671e-05, -8.36754580e-06, -1.43387447e-05], … [-6.46738552e-06, -1.19701456e-05, -2.41882630e-05, …, -2.31175915e-05, -2.37374262e-05, -7.53926446e-06], [ 2.91850049e-05, 2.81713398e-05, 5.17618757e-05, …, 4.44764295e-05, 4.08778688e-05, 3.83233192e-05], [-1.80642354e-05, -1.06068608e-05, -3.71177368e-06, …, 1.48063260e-05, 1.48063260e-05, 1.48063260e-05]],

      [[ 4.42062699e-05, 5.99208645e-05, 7.60618853e-05, …, 9.85508013e-05, 8.87923195e-05, 8.02998878e-05], [-6.30930699e-06, 2.14882295e-05, 5.40961203e-06, …, -6.18517313e-05, -8.49961217e-05, -7.07025406e-05], [ 1.32347041e-04, 9.66830285e-05, 9.77152893e-05, …, -3.68393584e-05, -2.08663734e-05, -1.27901811e-05], …, [ 7.59530363e-07, -9.00525049e-06, -1.95382678e-05, …, -2.78150664e-05, -1.46349189e-05, 3.65395950e-06], [ 3.22381374e-05, 3.90163375e-05, 6.39709157e-05, …, 5.17422904e-05, 4.94353600e-05, 3.90770304e-05], [-2.50190016e-05, -1.82145705e-05, -1.97245214e-05, …, 6.13148062e-06, 6.13148062e-06, 6.13148062e-06]]])

    • csd_V1
      (channel_depth, trial_nr, time_csd)
      float64
      -0.04729 -0.04843 … 0.004271
      array([[[-0.04729251, -0.04843248, -0.03745331, …, -0.0506883 ,
      -0.05553956, -0.06003479],
      [-0.01093619,  0.01574628,  0.00914164, …,  0.00598613,
      0.01101591,  0.03123014],
      [-0.03696462, -0.04099173, -0.03852095, …,  0.00098069,
      -0.0050393 , -0.01264635],
      …,
      [-0.01247878,  0.00877942,  0.02265446, …,  0.00331111,
      0.03482657,  0.01182155],
      [-0.00745365, -0.01184343, -0.0143719 , …, -0.00701411,
      -0.00677321,  0.00801698],
      [-0.00185681,  0.00029347,  0.01247577, …,  0.04692436,
      0.04692436,  0.04692436]],

      [[-0.05243487, -0.04187987, -0.01951768, …, 0.00767212, 0.02122529, -0.00091668], [-0.03800667, -0.0267893 , 0.00151346, …, -0.06356118, -0.05255179, -0.02253284], [-0.13918267, -0.15069373, -0.14869443, …, -0.08287076, -0.10533221, -0.11878146], … [-0.0317182 , -0.0291328 , -0.04440809, …, 0.0128667 , -0.00803555, 0.00140582], [-0.00313798, -0.02211998, 0.00973861, …, 0.01834821, 0.02898202, 0.03053275], [ 0.01935376, 0.02458448, 0.02770717, …, 0.0335107 , 0.0335107 , 0.0335107 ]],

      [[ 0.05527245, 0.06647928, 0.08365367, …, 0.03946655, -0.00638597, 0.01617121], [ 0.00484864, 0.04955322, 0.00056809, …, -0.02495747, -0.04720462, -0.05647099], [ 0.14248465, 0.09267001, 0.12532577, …, -0.02102215, 0.01070787, -0.00105385], …, [ 0.02074605, 0.01291679, -0.00810403, …, -0.00440359, 0.01890968, 0.03686672], [-0.03145411, -0.01193072, 0.05258549, …, 0.06270049, 0.05033156, 0.03718263], [-0.01626375, -0.00653823, -0.01209703, …, 0.00427089, 0.00427089, 0.00427089]]])

    • channel_depth
      PandasIndex
      PandasIndex(Index([   0,  -40,  -80, -120, -160, -200, -240, -280, -320, -360, -400, -440,
      -480, -520, -560, -600, -640, -680, -720, -760, -800, -840, -880],
      dtype='int64', name='channel_depth'))
    • trial_nr
      PandasIndex
      PandasIndex(Index([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17,
      18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
      36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53,
      54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
      72, 73, 74],
      dtype='int32', name='trial_nr'))
    • time_lfp
      PandasIndex
      PandasIndex(Index([           -1000.0, -999.1999999999999,             -998.4,
      -997.5999999999999,             -996.8, -995.9999999999999,
      -995.1999999999998, -994.3999999999999, -993.5999999999998,
      -992.7999999999998,
      …
      492.00000000004263, 492.80000000004276,  493.6000000000429,
      494.4000000000428, 495.20000000004273, 496.00000000004286,
      496.800000000043,  497.6000000000429, 498.40000000004284,
      499.20000000004296],
      dtype='float64', name='time_lfp', length=1875))
    • time_csd
      PandasIndex
      PandasIndex(Index([           -1000.0, -999.1999999999999,             -998.4,
      -997.5999999999999,             -996.8, -995.9999999999999,
      -995.1999999999998, -994.3999999999999, -993.5999999999998,
      -992.7999999999998,
      …
      492.00000000004263, 492.80000000004276,  493.6000000000429,
      494.4000000000428, 495.20000000004273, 496.00000000004286,
      496.800000000043,  497.6000000000429, 498.40000000004284,
      499.20000000004296],
      dtype='float64', name='time_csd', length=1875))
  • time_unit :
    millisecond
    lfp_unit :
    Volt
    channel_depth_unit :
    micrometer
    note_channel_depth :
    Measured in distance from electrode closest to scalp
    sampling_frequency_lfp :
    1250
    sampling_frequency_spikes :
    1000
    sampling_frequency_unit :
    Hz
    stimulus_onset :
    1000
    stimulus_offset :
    1250

Example: Calculate and plot the CSD for the first trial (trial index 0). You can get the lfp from the first trial using traditional array indexing (e.g. lfp_V1[:, 0]) or the sel method (e.g. lfp_V1.sel(trial_nr = 0)).

lfp = dataset['lfp_V1'].sel(trial_nr = 0)
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth, method='StandardCSD')
utils.plot_csd(csd)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Exercise: Calculate and plot the CSD for trial at index 30.

Solution
lfp = dataset['lfp_V1'].sel(trial_nr = 30)
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth, method='StandardCSD')
utils.plot_csd(csd)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Exercise: Calculate and plot the CSD for the trial averaged LFP (Tip: lfp.mean(dim='trial_nr'))

Solution
lfp = dataset['lfp_V1'].mean(dim = 'trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth, method='StandardCSD')
utils.plot_csd(csd)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Example: Using the gaussian_filter() function, smooth the CSD from across depth (over the channels) with a gaussian filter with sigma = (0,1). (The 0 means that no smoothing is across over the time dimension.) Plot the CSD after smoothing with the pcolormesh function.

from scipy.ndimage import gaussian_filter
lfp = dataset['lfp_V1'].mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth)
csd_smoothed = gaussian_filter(csd, sigma=(1, 0))
utils.plot_csd(csd_smoothed)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Exercise: Smooth the CSD across depth (over the channels) with a gaussian filter with sigma = (0,4). Plot the CSD after smoothing.

What happened to the minor and the major sinks and sources in the plot when you compare to CSD plots with no or less smoothing?

Solution
lfp = dataset['lfp_V1'].mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth)
csd_smoothed = gaussian_filter(csd, sigma=(0, 4))
utils.plot_csd(csd_smoothed)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Exercise: Smooth the CSD across time with a gaussian filter with sigma = (4,0). Plot the CSD after smoothing.

Solution
lfp = dataset['lfp_V1'].mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth)
csd_smoothed = gaussian_filter(csd, sigma=(4, 0))
utils.plot_csd(csd_smoothed)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Exercise: Smooth the CSD across depth and time with a gaussian filter with sigma = (2,2). Plot the CSD after smoothing.

Solution
lfp = dataset['lfp_V1'].mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth)
csd_smoothed = gaussian_filter(csd, sigma=(2, 2))
utils.plot_csd(csd_smoothed)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Comment: While smoothing and interpolation can improve your visalization by making your plots more readable, excessive smoothing can make them misleading, so use with care.

Section 2: Slicing Time Points, Plotting Time, and Making 2D Plots Using plt.pcolormesh()

Code Description
mask = dataset['time_lfp'] > 0 Create a logical mask
subdata = dataset.where(mask, drop=True) Use the mask to extract data
dataset.mean(dim='trial_nr) Calculate the trial average.
plt.pcolormesh(x, y, C, cmap = 'plasma', shading = 'gouraud') Make a 2D colormap of values in a 2D array (C) against x and y values. The optional parameters cmap and shading defines the colormap and the interpolation applied to the plot, respectively.

Exercises

Example: Extract and plot the CSD calculated from the trial averaged LFP from stimulus onset to the end of the trial.

mask = dataset.time_lfp > 0
lfp = dataset['lfp_V1'].where(mask, drop=True).mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth, method='StandardCSD')
utils.plot_csd(csd)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Exercise: Extract and plot the CSD calculated from the trial averaged LFP from the beginning of the trial to the stimulus onset.

Solution
mask = dataset.time_lfp > -1000
lfp = dataset['lfp_V1'].where(mask, drop=True).mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth, method='StandardCSD')
utils.plot_csd(csd)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Exercise: Extract and plot the CSD from stimulus onset until 100 msec after stimulus onset.

Approximately when does the first current sink (blue blob) arise? Is the position of this blob between channels 5 and 10 - roughly layer 4 of the primary visual cortex? If so, the CSD plot is correct.

Useful Code:

mask = (0 < dataset['time_lfp']) & (dataset['time_lfp'] < 100)
Solution
mask = (0 < dataset.time_lfp) & (dataset.time_lfp < 100)
lfp = dataset['lfp_V1'].where(mask, drop=True).mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth)
utils.plot_csd(csd)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Example: Plot the trial averaged CSD in the first 100 ms after stimulus onset using plt.pcolormesh function. Pass the time points that the LFP was sampled as the first parameter and the depth of channels (channels_depth) as the second parameter to the pcolormesh function.

mask = (0 < dataset.time_lfp) & (dataset.time_lfp < 100)
lfp = dataset['lfp_V1'].where(mask, drop=True).mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth)

plt.figure()
plt.pcolormesh(lfp.time_lfp, dataset.channel_depth, csd.T, cmap = 'RdBu_r')
plt.xlabel('Time (ms)')
plt.ylabel(r'Depth ($\mu$m)');
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Exercise: Plot the trial averaged CSD in the first 250 ms after stimulus onset using plt.pcolormesh function. Pass the time points that the LFP was sampled as the first parameter and the depth of channels (channels_depth) as the second parameter to the pcolormesh function.

Solution
mask = (0 < dataset.time_lfp) & (dataset.time_lfp < 250)
lfp = dataset['lfp_V1'].where(mask, drop=True).mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth)

plt.figure()
plt.pcolormesh(lfp.time_lfp, dataset.channel_depth, csd.T, cmap = 'RdBu_r')
plt.xlabel('Time (ms)')
plt.ylabel(r'Depth ($\mu$m)');
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Exercise: Get the CSD from the from 200 ms before stimulus onset to 200 ms after stimulus onset and plot it using plt.pcolormesh function.

Solution
mask = (-200 < dataset.time_lfp) & (dataset.time_lfp < 200)
lfp = dataset['lfp_V1'].where(mask, drop=True).mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth)

plt.figure()
plt.pcolormesh(lfp.time_lfp, dataset.channel_depth, csd.T, cmap = 'RdBu_r')
plt.xlabel('Time (ms)')
plt.ylabel(r'Depth ($\mu$m)');
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Section 3: Calculate the Current Source Density (CSD) from the Recorded LFP Using the Delta Source Method.

The method you used above to compute the CSD is called the “standard” method simply because it was developed first. The standard method assumes that the CSD is infinitely constant lateral to the probe, which does not always hold true. Sometimes, this approximation is good enough to not cause any major errors in the CSD estimate, but in other cases it can result in an incorrect CSD amplitude and to spurious current sinks and sources in the estimate.

One alternative method that has been developed to deal with this issue is called the “delta source” method, which only assumes that the CSD is constant within a certain diameter lateral to the probe. In this section, you will explore the calculation of CSD using the delta source method.

Code Description
utils.calc_csd(lfp, sampling_freq, channel_depths, method = 'DeltaiCSD', diam = diameter*pq.um) Calculates a CSD using the DeltaiCSD method. diam is the diameter that the CSD is assumed to be constant lateral to the probe.
pq.um Defines the quantities unit micrometer.
utils.plot_csd(csd) Plots the CSD
dataset.mean(dim='trial_stimulus_id) Calculate the trial average.

Exercises

Example: Calculate and plot the CSD for the trial averaged LFP using Delta iCSD method rather than the standard method.

Set the method parameter in the utils.calc_csd function to DeltaiCSD. Set the diam parameter to 800*pq.um.

diameter = 800*pq.um
lfp = dataset['lfp_V1'].mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth, method='DeltaiCSD', diam=diameter)
utils.plot_csd(csd)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Exercise: Change the diameter parameter to 200 μm\mu m and calculate and plot the the CSD again. What changed?

Solution
diameter = 200*pq.um
lfp = dataset['lfp_V1'].mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth, method='DeltaiCSD', diam=diameter)
utils.plot_csd(csd)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Exercise: Change the diameter parameter to 1600 μm\mu m and calculate and plot the the CSD again. What changed?

Solution
diameter = 1600*pq.um
lfp = dataset['lfp_V1'].mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth, method='DeltaiCSD', diam=diameter)
utils.plot_csd(csd)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Exercise: Calculate the CSD with the standard method (method='StandardCSD') and plot it again.

Which of the three plots above (where the CSD was calculated with the delta method with different diameters) does this CSD plot resemble the most? Is it the plot you would expect, considering the diameter used with the delta method in that plot?

Solution
lfp = dataset['lfp_V1'].mean(dim='trial_nr')
csd = utils.calc_csd(lfp, dataset.sampling_frequency_lfp, dataset.channel_depth, method='StandardCSD')
utils.plot_csd(csd)
discrete filter coefficients: 
b = [ 0.607 1.000 0.607 ],                
a = [ 2.213 ]

Comment: As you saw above, the diameter parameter you set for the delta method will affect the amplitude and the distribution of sinks and sources in the CSD estimate. Usually, one won’t exactly know the correct value for diameter. You have to settle for a reasonable estimate based on the physiology of the structure in question. For the cortex, a reasonable value to use is the diameter of a cortical column, which is approximately 300-500 micrometer in mice.

Section 4: Metrics to Quantify CSD Similarity: Correlation, Euclidian Distance, and Wasserstein Distance.

Sometimes you want to compare the CSD from different trials or different animals. Visualization is a great way to do that, but you may need to quantify the level of similarity between CSD plots too. Below are a few methods that can be used for that.

Code Description
data.flatten() Flattens a multidimensional array into a vector
pearsonr(array_1.flatten(), array2.flatten()) Calculates the Pearson correlation between two vectors.
plt.scatter(x, y) Make a scatter plot.
np.linalg.norm(array_1 - array2) Calculates the eucledian distance between two arrays.
wasserstein_distance(array_1.flatten(), array2.flatten()) Calculates the Wasserstein distance (WD) (also called the earth mover distance) between two (flattened) arrays. The smaller the Wasserstein distance is, the more similar the arrays are.
utils.correlate_pairwise_trials(csd) Calculate pairwise correlation between csd from all trials.
utils.euclidian_norm_pairwise_trials(csd) Calculate the pairwise euclidian distance between csd from all trials.

Exercises

from scipy.stats import wasserstein_distance, pearsonr

Run the cell below to get pre-calculated CSD from all trials and select the CSD from the first 100 ms after stimulus onset.

time_window_start = 0 # at stimulus onset
time_window_end = 100

# load precomputed csd in all trials form dataset
csd = dataset['csd_V1']
time_csd = csd.time_csd
mask_time_window = np.logical_and(time_window_start < time_csd, 
                                  time_window_end > time_csd)

csd = csd[:,:,mask_time_window]
csd.shape
(23, 75, 125)

Example: Make a scatter plot of the csd values in trial 20 and trial 21

# provided
csd_trialA = csd.sel(trial_nr = 20).values.flatten()
csd_trialB = csd.sel(trial_nr = 21).values.flatten()

plt.scatter(csd_trialA, csd_trialB, marker='.', s=1.5);

Exercise: Calculate and display the correlation between the CSD from trial 20 and trial 21.

Solution
pearsonr(csd_trialA, csd_trialB)
PearsonRResult(statistic=0.6614942496566328, pvalue=0.0)

Exercise: Calculate the correlation between the CSD from trial 0 and the last trial (set itrialB to 74).

Are these trials more or less similar than the neighboring trials in the previous exercise? Make a scatter plot where you plot the values of the CSD in trial 0 against the values of the CSD in trial 74 to check that the result makes sense.

Solution
csd_trialA = csd.sel(trial_nr = 0).values.flatten()
csd_trialB = csd.sel(trial_nr = 74).values.flatten()

plt.scatter(csd_trialA, csd_trialB, marker='.', s=1.5);
pearsonr(csd_trialA, csd_trialB)
PearsonRResult(statistic=0.10428248676854059, pvalue=2.0900224966659755e-08)

Exercise: Calculate the correlation between the CSD from the first trial and the trial averaged CSD.

Solution
pearsonr(csd_trialA.flatten(), np.mean(csd, axis=1).values.flatten())
PearsonRResult(statistic=0.24234596134818845, pvalue=1.0600984623522463e-39)

Demo: Run the cell below to calculate and plot the pairwise correlation between the CSD from all trials.

Does this plot suggest that neighboring trials are typically more correlated?

corr_csd_all_trials = utils.correlate_pairwise_trials(csd.values)

plt.imshow(corr_csd_all_trials, cmap = 'bwr')
plt.xlabel('Trial nr.')
plt.ylabel('Trial nr.')
plt.colorbar();

Exercise: Calculate the euclidian distance between the CSD from the first and the second trial.

Solution
csd_trialA = csd.sel(trial_nr = 0).values.flatten()
csd_trialB = csd.sel(trial_nr = 1).values.flatten()

np.linalg.norm(csd_trialA - csd_trialB)
6.763006171185026

Exercise: Calculate the euclidian distance between the CSD from trial 0 and the last trial.

Solution
csd_trialA = csd.sel(trial_nr = 0).values.flatten()
csd_trialB = csd.sel(trial_nr = 74).values.flatten()

np.linalg.norm(csd_trialA - csd_trialB)
7.175334725036059

Exercise: Calculate and plot the pairwise euclidian distance between the the CSD from all trials.

An important difference between the euclidian distance and the correlation is that amplitude differences also matters for the euclidian distance, so this plot will not necessarily have the same structure as the correlation plot above.

Solution
csd_pairwise_euclid = utils.euclidian_norm_pairwise_trials(csd)

plt.imshow(csd_pairwise_euclid, cmap = 'bwr')
plt.xlabel('Trial nr.')
plt.ylabel('Trial nr.')
plt.colorbar();

Exercise: Calculate the Wasserstein distance between the CSD from the first and the second trial.

Solution
csd_trialA = csd.sel(trial_nr = 0).values.flatten()
csd_trialB = csd.sel(trial_nr = 1).values.flatten()

wasserstein_distance(csd_trialA.flatten(), csd_trialB.flatten())
0.04407472847070443

Exercise: Calculate the Wasserstein distance between the CSD from the first and the last trial.

Solution
csd_trialA = csd.sel(trial_nr = 0).values.flatten()
csd_trialB = csd.sel(trial_nr = 74).values.flatten()

wasserstein_distance(csd_trialA.flatten(), csd_trialB.flatten())
0.03141649491519594

Discussion Exercise:

  • What are some potential reasons for using one or another metric to quantify similarity between two CSD plots?