Tuning curves#

Pynapple can compute 1 dimension tuning curves (for example firing rate as a function of angular direction) and 2 dimension tuning curves ( for example firing rate as a function of position). It can also compute average firing rate for different epochs (for example firing rate for different epochs of stimulus presentation).

Important

If you are using calcium imaging data with the activity of the cell as a continuous transient, the function to call ends with _continuous for continuous time series (e.g. compute_1d_tuning_curves_continuous).

Hide code cell content
import pynapple as nap
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from pprint import pprint
custom_params = {"axes.spines.right": False, "axes.spines.top": False}
sns.set_theme(style="ticks", palette="colorblind", font_scale=1.5, rc=custom_params)
Hide code cell content
group = {
    0: nap.Ts(t=np.sort(np.random.uniform(0, 100, 10))),
    1: nap.Ts(t=np.sort(np.random.uniform(0, 100, 20))),
    2: nap.Ts(t=np.sort(np.random.uniform(0, 100, 30))),
}

tsgroup = nap.TsGroup(group)

from epochs#

The epochs should be stored in a dictionnary :

dict_ep =  {
                "stim0": nap.IntervalSet(start=0, end=20),
                "stim1":nap.IntervalSet(start=30, end=70)
            }

nap.compute_discrete_tuning_curves takes a TsGroup for spiking activity and a dictionary of epochs. The output is a pandas DataFrame where each column is a unit in the TsGroup and each row is one IntervalSet type. The value is the mean firing rate of the neuron during this set of intervals.

mean_fr = nap.compute_discrete_tuning_curves(tsgroup, dict_ep)

pprint(mean_fr)
           0     1      2
stim0  0.050  0.40  0.250
stim1  0.175  0.15  0.375

from timestamps activity#

1-dimension tuning curves#

Hide code cell content
from scipy.ndimage import gaussian_filter1d

# Fake Tuning curves
N = 6 # Number of neurons
bins = np.linspace(0, 2*np.pi, 61)
x = np.linspace(-np.pi, np.pi, len(bins)-1)
tmp = np.roll(np.exp(-(1.5*x)**2), (len(bins)-1)//2)
generative_tc = np.array([np.roll(tmp, i*(len(bins)-1)//N) for i in range(N)]).T

# Feature
T = 50000
dt = 0.002
timestep = np.arange(0, T)*dt
feature = nap.Tsd(
    t=timestep,
    d=gaussian_filter1d(np.cumsum(np.random.randn(T)*0.5), 20)%(2*np.pi)
    )
index = np.digitize(feature, bins)-1

# Spiking activity
count = np.random.poisson(generative_tc[index])>0
tsgroup = nap.TsGroup(
    {i:nap.Ts(timestep[count[:,i]]) for i in range(N)},
    time_support = nap.IntervalSet(0, 100)
    )

Mandatory arguments are TsGroup, Tsd (or TsdFrame with 1 column only) and nb_bins for number of bins of the tuning curves.

If an IntervalSet is passed with ep, everything is restricted to ep otherwise the time support of the feature is used.

The min and max of the tuning curve is by default the min and max of the feature. This can be tweaked with the argument minmax.

The output is a pandas DataFrame. Each column is a unit from TsGroup argument. The index of the DataFrame carries the center of the bin in feature space.

tuning_curve = nap.compute_1d_tuning_curves(
    group=tsgroup,
    feature=feature,
    nb_bins=120, 
    minmax=(0, 2*np.pi)
    )

print(tuning_curve)
                   0           1    2    3         4           5
0.026180  322.545311   40.156244  0.0  0.0  0.000000   27.202617
0.078540  314.362723   49.505941  0.0  0.0  0.000000   28.465916
0.130900  286.792523   81.082703  0.0  0.0  0.000000   21.021441
0.183260  289.688333   80.689444  0.0  0.0  0.000000   13.227778
0.235619  280.862040  102.017154  0.0  0.0  0.000000   10.075768
...              ...         ...  ...  ...       ...         ...
6.047566  263.240559   11.764941  0.0  0.0  1.470618  114.708177
6.099926  300.567809   11.236180  0.0  0.0  0.000000   87.080393
6.152286  287.445362   16.908551  0.0  0.0  0.000000   61.595435
6.204645  314.560282   39.906901  0.0  0.0  0.000000   44.601831
6.257005  300.517519   31.969949  0.0  0.0  0.000000   40.921535

[120 rows x 6 columns]
plt.figure()
plt.plot(tuning_curve)
plt.xlabel("Feature space")
plt.ylabel("Firing rate (Hz)")
plt.show()
../_images/c558a5134fc972c152239706b7f6ccbc82ccf593c7b264004625d588d7f48cd0.png

Internally, the function is calling the method value_from which maps a timestamps to its closest value in time from a Tsd object. It is then possible to validate the tuning curves by displaying the timestamps as well as their associated values.

Hide code cell source
plt.figure()
plt.subplot(121)
plt.plot(tsgroup[3].value_from(feature), 'o')
plt.plot(feature, label="feature")
plt.ylabel("Feature")
plt.xlim(0, 2)
plt.xlabel("Time (s)")
plt.subplot(122)
plt.plot(tuning_curve[3].values, tuning_curve[3].index.values, label="Tuning curve (unit=3)")
plt.xlabel("Firing rate (Hz)")
plt.legend()
plt.show()
../_images/06491d9cecaac53278dfbb45a0211d6aaafb198b6875bbc72ef34ddd9a1f9fd8.png

2-dimension tuning curves#

Hide code cell content
dt = 0.01
T = 10
epoch = nap.IntervalSet(start=0, end=T, time_units="s")
features = np.vstack((np.cos(np.arange(0, T, dt)), np.sin(np.arange(0, T, dt)))).T
features = nap.TsdFrame(
    t=np.arange(0, T, dt),
    d=features,
    time_units="s",
    time_support=epoch,
    columns=["a", "b"],
)
tsgroup = nap.TsGroup({
    0: nap.Ts(t=np.sort(np.random.uniform(0, T, 10))),
    1: nap.Ts(t=np.sort(np.random.uniform(0, T, 15))),
    2: nap.Ts(t=np.sort(np.random.uniform(0, T, 20))),
}, time_support=epoch)

The group argument must be a TsGroup object. The features argument must be a 2-columns TsdFrame object. nb_bins can be an int or a tuple of 2 ints.

tcurves2d, binsxy = nap.compute_2d_tuning_curves(
    group=tsgroup, 
    features=features, 
    nb_bins=(5,5),
    minmax=(-1, 1, -1, 1)
)
pprint(tcurves2d)
{0: array([[0.        , 0.        , 0.        , 1.12359551, 0.        ],
       [0.        ,        nan,        nan,        nan, 1.13636364],
       [2.5       ,        nan,        nan,        nan, 0.        ],
       [0.        ,        nan,        nan,        nan, 0.        ],
       [0.        , 4.44444444, 0.        , 4.54545455, 1.75438596]]),
 1: array([[0.        , 3.7037037 , 1.25      , 1.12359551, 3.50877193],
       [2.22222222,        nan,        nan,        nan, 1.13636364],
       [0.        ,        nan,        nan,        nan, 0.        ],
       [2.27272727,        nan,        nan,        nan, 1.13636364],
       [3.57142857, 0.        , 3.27868852, 0.        , 1.75438596]]),
 2: array([[0.        , 2.4691358 , 1.25      , 2.24719101, 1.75438596],
       [2.22222222,        nan,        nan,        nan, 3.40909091],
       [2.5       ,        nan,        nan,        nan, 2.4691358 ],
       [4.54545455,        nan,        nan,        nan, 2.27272727],
       [0.        , 2.22222222, 0.        , 1.13636364, 1.75438596]])}
/home/runner/.local/lib/python3.12/site-packages/pynapple/process/tuning_curves.py:269: RuntimeWarning: invalid value encountered in divide
  count = count / occupancy

tcurves2d is a dictionnary with each key a unit in TsGroup. binsxy is a numpy array representing the centers of the bins and is useful for plotting tuning curves. Bins that have never been visited by the feature have been assigned a NaN value.

Checking the accuracy of the tuning curves can be bone by displaying the spikes aligned to the features with the function value_from which assign to each spikes the corresponding features value for unit 0.

ts_to_features = tsgroup[0].value_from(features)
print(ts_to_features)
Time (s)           a         b
----------  --------  --------
0.291539     0.95824   0.28595
0.388992     0.92491   0.38019
0.455699     0.89605   0.44395
0.819643     0.68222   0.73115
4.53104     -0.18138  -0.98341
5.9385       0.94169  -0.33649
6.0642       0.9752   -0.22134
6.8445       0.84894   0.52848
8.39921     -0.51929   0.8546
8.97013     -0.89836   0.43926
dtype: float64, shape: (10, 2)

tsgroup[0] which is a Ts object has been transformed to a TsdFrame object with each timestamps (spike times) being associated with a features value.

Hide code cell source
plt.figure()
plt.subplot(121)
plt.plot(features["b"], features["a"], label="features")
plt.plot(ts_to_features["b"], ts_to_features["a"], "o", color="red", markersize=4, label="spikes")
plt.xlabel("feature b")
plt.ylabel("feature a")
[plt.axvline(b, linewidth=0.5, color='grey') for b in np.linspace(-1, 1, 6)]
[plt.axhline(b, linewidth=0.5, color='grey') for b in np.linspace(-1, 1, 6)]
plt.subplot(122)
extents = (
    np.min(features["a"]),
    np.max(features["a"]),
    np.min(features["b"]),
    np.max(features["b"]),
)
plt.imshow(tcurves2d[0], 
    origin="lower", extent=extents, cmap="viridis",
    aspect='auto'
    )
plt.title("Tuning curve unit 0")
plt.xlabel("feature b")
plt.ylabel("feature a")
plt.grid(False)
plt.colorbar()
plt.tight_layout()
plt.show()
../_images/fbdaa91995b41f0fd035f0d66e4afa3a15b752fe2b16a12a96a5e993f0eee7c0.png

from continuous activity#

Tuning curves compute with the following functions are usually made with data from calcium imaging activities.

1-dimension tuning curves#

Hide code cell content
from scipy.ndimage import gaussian_filter1d

# Fake Tuning curves
N = 3 # Number of neurons
bins = np.linspace(0, 2*np.pi, 61)
x = np.linspace(-np.pi, np.pi, len(bins)-1)
tmp = np.roll(np.exp(-(1.5*x)**2), (len(bins)-1)//2)
generative_tc = np.array([np.roll(tmp, i*(len(bins)-1)//N) for i in range(N)]).T

# Feature
T = 50000
dt = 0.002
timestep = np.arange(0, T)*dt
feature = nap.Tsd(
    t=timestep,
    d=gaussian_filter1d(np.cumsum(np.random.randn(T)*0.5), 20)%(2*np.pi)
    )
index = np.digitize(feature, bins)-1
tmp = generative_tc[index]
tmp = tmp + np.random.randn(*tmp.shape)*1
# Calcium activity
tsdframe = nap.TsdFrame(
    t=timestep,
    d=tmp
    )

Arguments are TsdFrame (for example continuous calcium data), Tsd or TsdFrame for the 1-d feature and nb_bins for the number of bins.

tuning_curves = nap.compute_1d_tuning_curves_continuous(
    tsdframe=tsdframe,
    feature=feature,
    nb_bins=120,
    minmax=(0, 2*np.pi)
    )

print(tuning_curves)
                 0         1         2
0.026180  1.033115 -0.037311 -0.020473
0.078540  1.062447 -0.035947  0.022877
0.130900  0.881437 -0.047208  0.021346
0.183260  0.917030  0.036087  0.010231
0.235619  0.782101 -0.042449  0.011075
...            ...       ...       ...
6.047566  0.882446  0.085013 -0.019371
6.099926  0.883461  0.025363  0.002948
6.152286  0.929687 -0.018051 -0.015350
6.204645  0.978697  0.016672  0.021866
6.257005  0.917792 -0.004740 -0.014152

[120 rows x 3 columns]
plt.figure()
plt.plot(tuning_curves)
plt.xlabel("Feature space")
plt.ylabel("Mean activity")
plt.show()
../_images/3872a221b0ac3b4dbbfbf9cc1f548918a26f32efe6194dac118a2f1cdaa6dd80.png

2-dimension tuning curves#

Hide code cell content
dt = 0.01
T = 10
epoch = nap.IntervalSet(start=0, end=T, time_units="s")
features = np.vstack((np.cos(np.arange(0, T, dt)), np.sin(np.arange(0, T, dt)))).T
features = nap.TsdFrame(
    t=np.arange(0, T, dt),
    d=features,
    time_units="s",
    time_support=epoch,
    columns=["a", "b"],
)


# Calcium activity
tsdframe = nap.TsdFrame(
    t=timestep,
    d=np.random.randn(len(timestep), 2)
    )

Arguments are TsdFrame (for example continuous calcium data), Tsd or TsdFrame for the 1-d feature and nb_bins for the number of bins.

tuning_curves, xy = nap.compute_2d_tuning_curves_continuous(
    tsdframe=tsdframe,
    features=features,
    nb_bins=5,    
    )

print(tuning_curves)
{0: array([[ 0.06650352, -0.04070614, -0.00703202, -0.04453042,  0.08696992],
       [ 0.02741139,         nan,         nan,         nan,  0.01391568],
       [ 0.11496776,         nan,         nan,         nan, -0.04714853],
       [-0.02226617,         nan,         nan,         nan, -0.05965593],
       [-0.21804952, -0.00762013,  0.0829199 , -0.05734175, -0.06100247]]), 1: array([[-0.16020866, -0.01218332,  0.00055747, -0.11688487,  0.02871264],
       [ 0.07734304,         nan,         nan,         nan, -0.01020518],
       [ 0.02444997,         nan,         nan,         nan, -0.02323832],
       [-0.13164026,         nan,         nan,         nan,  0.07960367],
       [-0.02267051, -0.04346854, -0.08652299, -0.0098474 , -0.03474793]])}
/home/runner/.local/lib/python3.12/site-packages/numpy/_core/fromnumeric.py:3596: RuntimeWarning: Mean of empty slice.
  return _methods._mean(a, axis=axis, dtype=dtype,
/home/runner/.local/lib/python3.12/site-packages/numpy/_core/_methods.py:130: RuntimeWarning: invalid value encountered in divide
  ret = um.true_divide(
plt.figure()
plt.subplot(121)
plt.plot(features["b"], features["a"], label="features")
plt.xlabel("feature b")
plt.ylabel("feature a")
[plt.axvline(b, linewidth=0.5, color='grey') for b in np.linspace(-1, 1, 6)]
[plt.axhline(b, linewidth=0.5, color='grey') for b in np.linspace(-1, 1, 6)]
plt.subplot(122)
extents = (
    np.min(features["a"]),
    np.max(features["a"]),
    np.min(features["b"]),
    np.max(features["b"]),
)
plt.imshow(tuning_curves[0], 
    origin="lower", extent=extents, cmap="viridis",
    aspect='auto'
    )
plt.title("Tuning curve unit 0")
plt.xlabel("feature b")
plt.ylabel("feature a")
plt.grid(False)
plt.colorbar()
plt.tight_layout()
plt.show()
../_images/b0cdf3ff6e9b3f4f4bad7cc79c6dc5afc9ca3330c01117efb1670309702772b8.png