Skip to content

Note

Click here to download the full example code

Calcium Imaging

Working with calcium data.

For the example dataset, we will be working with a recording of a freely-moving mouse imaged with a Miniscope (1-photon imaging). The area recorded for this experiment is the postsubiculum - a region that is known to contain head-direction cells, or cells that fire when the animal's head is pointing in a specific direction.

The NWB file for the example is hosted on OSF. We show below how to stream it.

See the documentation of Pynapple for instructions on installing the package.

This tutorial was made by Sofia Skromne Carrasco and Guillaume Viejo.

Warning

This tutorial uses seaborn and matplotlib for displaying the figure

You can install all with pip install matplotlib seaborn tqdm

mkdocs_gallery_thumbnail_number = 1

Now, import the necessary libraries:

import numpy as pd
import pynapple as nap
import matplotlib.pyplot as plt
import seaborn as sns
import sys, os
import requests, math
import tqdm

custom_params = {"axes.spines.right": False, "axes.spines.top": False}
sns.set_theme(style="ticks", palette="colorblind", font_scale=1.5, rc=custom_params)

Downloading the data

First things first: Let's find our file

path = "A0670-221213.nwb"
if path not in os.listdir("."):
  r = requests.get(f"https://osf.io/sbnaw/download", stream=True)
  block_size = 1024*1024
  with open(path, 'wb') as f:
    for data in tqdm.tqdm(r.iter_content(block_size), unit='MB', unit_scale=True,
      total=math.ceil(int(r.headers.get('content-length', 0))//block_size)):
      f.write(data)

Parsing the data

Now that we have the file, let's load the data

data = nap.load_file(path)
print(data)

Out:

A0670-221213
┍━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━┑
 Keys                   Type        ┝━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━┥
 position_time_support  IntervalSet  RoiResponseSeries      TsdFrame     z                      Tsd          y                      Tsd          x                      Tsd          rz                     Tsd          ry                     Tsd          rx                     Tsd         ┕━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━┙

Let's save the RoiResponseSeries as a variable called 'transients' and print it

transients = data['RoiResponseSeries']
print(transients)

Out:

Time (s)          0        1        2        3        4  ...
----------  -------  -------  -------  -------  -------  -----
3.1187      0.27546  0.79973  0.16383  0.20118  0.02926  ...
3.15225     0.26665  0.86751  0.15879  0.23682  0.02719  ...
3.18585     0.25796  0.89419  0.15352  0.25074  0.03651  ...
3.2194      0.24943  0.89513  0.14812  0.25215  0.05627  ...
3.253       0.24111  0.88023  0.14898  0.24651  0.07095  ...
3.28655     0.233    0.85584  0.14858  0.23706  0.08147  ...
3.32015     0.22513  1.0996   0.14715  0.22572  0.08859  ...
3.35375     0.2175   1.3521   0.1449   0.2136   0.09296  ...
3.3873      0.21011  1.484    0.14201  0.20137  0.09512  ...
3.42085     0.20296  1.5394   0.13861  0.18938  0.09552  ...
3.45445     0.19837  1.5469   0.13482  0.17783  0.09456  ...
3.488       0.19323  1.5248   0.13076  0.21747  0.09252  ...
3.5216      0.18776  1.4848   0.1265   0.23413  0.08969  ...
3.55515     0.18213  1.4345   0.1221   0.23749  0.08627  ...
3.58875     0.17646  1.3788   0.11764  0.23332  0.08244  ...
3.6223      0.1708   1.3206   0.11315  0.22503  0.07833  ...
3.6559      0.16523  1.262    0.10867  0.21464  0.07406  ...
3.68945     0.15976  1.2042   0.10424  0.20335  0.06973  ...
3.72305     0.15443  1.1477   0.09987  0.19183  0.06886  ...
3.7566      0.14924  1.0932   0.09559  0.18049  0.06837  ...
...
1202.953    0.14054  0.18209  0.05769  0.08317  0.00751  ...
1202.9866   0.14841  0.17342  0.07786  0.07791  0.00682  ...
1203.02015  0.15216  0.16509  0.09362  0.07298  0.00619  ...
1203.05375  0.15309  0.15711  0.10571  0.06835  0.00562  ...
1203.0873   0.15212  0.14948  0.11472  0.06401  0.00509  ...
1203.12085  0.18472  0.14221  0.12117  0.05995  0.00461  ...
1203.15445  0.2047   0.13527  0.1255   0.05614  0.00417  ...
1203.18805  0.21599  0.153    0.12807  0.05258  0.00378  ...
1203.2216   0.22132  0.17387  0.12921  0.08098  0.00342  ...
1203.25515  0.2226   0.1837   0.12918  0.09482  0.01982  ...
1203.28875  0.22113  0.18657  0.12819  0.10015  0.05878  ...
1203.3223   0.21785  0.1851   0.12644  0.10059  0.25022  ...
1203.3559   0.21338  0.181    0.12409  0.09827  0.44659  ...
1203.38945  0.20815  0.17535  0.12126  0.09446  0.87427  ...
1203.42305  0.20247  0.17243  0.11807  0.08992  1.2578   ...
1203.4566   0.19654  0.17056  0.11461  0.08508  1.62     ...
1203.4902   0.19052  0.16645  0.11096  0.0802   1.8811   ...
1203.52375  0.18449  0.16105  0.10717  0.07542  2.0599   ...
1203.55735  0.17851  0.15494  0.10331  0.07081  2.2176   ...
1203.5909   0.17264  0.14851  0.09942  0.06643  2.311    ...
dtype: float64, shape: (35757, 65)

Plotting the activity of one neuron

Our transients are saved as a (35757, 65) TsdFrame. Looking at the printed object, you can see that we have 35757 data points for each of our 65 regions of interest. We want to see which of these are head-direction cells, so we need to plot a tuning curve of fluorescence vs head-direction of the animal.

plt.figure(figsize=(6, 2))
plt.plot(transients[0:2000,0], linewidth=5)
plt.xlabel("Time (s)")
plt.ylabel("Fluorescence")
plt.show()

tutorial calcium imaging

Here we extract the head-direction as a variable called angle

angle = data['ry']
print(angle)

Out:

Time (s)
----------  -------
3.0994      2.58326
3.10775     2.5864
3.11605     2.5905
3.1244      2.59191
3.13275     2.59263
3.14105     2.59306
3.1494      2.59404
3.15775     2.59442
3.16605     2.59358
3.1744      2.59316
3.18275     2.59375
3.19105     2.59247
3.1994      2.58917
3.20775     2.58861
3.21605     2.58742
3.2244      2.58352
3.23275     2.58511
3.24105     2.58455
3.2494      2.58384
3.25775     2.58475
...
1206.0645   3.09176
1206.0728   3.13887
1206.08115  3.19664
1206.0895   3.26156
1206.0978   3.31936
1206.10615  3.37761
1206.1145   3.4264
1206.1228   3.46681
1206.13115  3.51726
1206.1395   3.58739
1206.1478   3.64066
1206.15615  3.68839
1206.1645   3.70594
1206.1728   3.70308
1206.18115  3.6908
1206.18945  3.69804
1206.1978   3.6728
1206.20615  3.65452
1206.21445  3.61199
1206.2228   3.5495
dtype: float64, shape: (144382,)

As you can see, we have a longer recording for our tracking of the animal's head than we do for our calcium imaging - something to keep in mind.

print(transients.time_support)
print(angle.time_support)

Out:

            start      end
       0   3.1187  1203.59
shape: (1, 2), time unit: sec.
            start      end
       0   3.0994  1206.22
shape: (1, 2), time unit: sec.

Calcium tuning curves

Here we compute the tuning curves of all the neurons

tcurves = nap.compute_1d_tuning_curves_continuous(transients, angle, nb_bins = 120)

print(tcurves)

Out:

                0         1   ...        63        64
0.026195  0.395699  0.055843  ...  0.090393  0.090931
0.078555  0.279695  0.052430  ...  0.112558  0.101200
0.130915  0.398603  0.044422  ...  0.092577  0.127856
0.183274  0.379213  0.043964  ...  0.071661  0.144850
0.235634  0.266577  0.038920  ...  0.070615  0.177883
...            ...       ...  ...       ...       ...
6.047557  0.390266  0.072893  ...  0.108395  0.080172
6.099916  0.266773  0.065594  ...  0.103724  0.081672
6.152276  0.268866  0.060269  ...  0.099209  0.083993
6.204636  0.281763  0.064460  ...  0.098601  0.088175
6.256995  0.293497  0.048092  ...  0.084487  0.100030

[120 rows x 65 columns]

We now have a DataFrame, where our index is the angle of the animal's head in radians, and each column represents the tuning curve of each region of interest. We can plot one neuron.

plt.figure()
plt.plot(tcurves[4])
plt.xlabel("Angle")
plt.ylabel("Fluorescence")
plt.show()

tutorial calcium imaging

It looks like this could be a head-direction cell. One important property of head-directions cells however, is that their firing with respect to head-direction is stable. To check for their stability, we can split our recording in two and compute a tuning curve for each half of the recording.

We start by finding the midpoint of the recording, using the function get_intervals_center. Using this, then create one new IntervalSet with two rows, one for each half of the recording.

center = transients.time_support.get_intervals_center()

halves = nap.IntervalSet(
    start = [transients.time_support.start[0], center.t[0]],
    end = [center.t[0], transients.time_support.end[0]]
    )

Now we can compute the tuning curves for each half of the recording and plot the tuning curves for the fifth region of interest.

half1 = nap.compute_1d_tuning_curves_continuous(transients, angle, nb_bins = 120, ep = halves.loc[[0]])
half2 = nap.compute_1d_tuning_curves_continuous(transients, angle, nb_bins = 120, ep = halves.loc[[1]])

plt.figure(figsize=(12, 5))
plt.subplot(1,2,1)
plt.plot(half1[4])
plt.title("First half")
plt.xlabel("Angle")
plt.ylabel("Fluorescence")
plt.subplot(1,2,2)
plt.plot(half2[4])
plt.title("Second half")
plt.show()

First half, Second half

Total running time of the script: ( 0 minutes 1.041 seconds)

Download Python source code: tutorial_calcium_imaging.py

Download Jupyter notebook: tutorial_calcium_imaging.ipynb

Gallery generated by mkdocs-gallery