Showing Receptive Fields#

Note: some of this content is adapted from the Allen SDK.

One of the most important features of visually responsive neurons is the location and extent of their receptive field. This is the region within the visual field in which stimulation can affect the neuron’s response. Is it highly localized or spatially distributed? Is it centered on the stimulus display, or is it on an edge? How much does it overlap with the receptive fields of neurons in other regions? Obtaining answers to these questions is a crucial step for interpreting results related to neurons’ visual coding properties. This notebook demonstrates how to use the ‘gabors’ stimulus table and the Units table to compute the receptive fields.

Environment Setup#

⚠️Note: If running on a new environment, run this cell once and then restart the kernel⚠️

import warnings
warnings.filterwarnings('ignore')

try:
    from databook_utils.dandi_utils import dandi_download_open
except:
    !git clone https://github.com/AllenInstitute/openscope_databook.git
    %cd openscope_databook
    %pip install -e .
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

%matplotlib inline

Download Ecephys File#

In this example, we use the Allen Institute’s Visual Coding - Neuropixels dataset. Change the code to download the file you’re interested in. Set dandiset_id and dandi_filepath to correspond to the dandiset id and filepath of the file, respectively. If you’re accessing an embargoed dataset, and set dandi_api_key to your DANDI API key.

dandiset_id = "000021"
dandi_filepath = "sub-716813540/sub-716813540_ses-739448407.nwb"
download_loc = "."
dandi_api_key = None
# This can sometimes take a while depending on the size of the file
io = dandi_download_open(dandiset_id, dandi_filepath, download_loc, dandi_api_key=dandi_api_key)
nwb = io.read()
A newer version (0.62.2) of dandi/dandi-cli is available. You are using 0.55.1
File already exists
Opening file

Reading Data#

We need two components of data for this analysis; The Units table, which is discussed more in depth in Visualizing Unit Quality Metrics. In particular we need the spike_times property for the units. We also need the stimulus table from the NWB File’s Intervals section. Because receptive field analysis is so central to interpreting results related to visual coding, experiments include some Gabor patches which are used to reliably drive neural responses, shown repeatedly at a set of different coordinates within the mouse’s visual field. This stimulus is usually shown at the beginning of the session, and typically uses the same parameters for every mouse. So for our purposes, we select the stimulus table named gabors_presentations. Below the Units table and the gabors stimulus table are shown.

units = nwb.units
units[:10]
peak_channel_id quality waveform_halfwidth amplitude velocity_below l_ratio snr amplitude_cutoff spread nn_miss_rate ... isolation_distance silhouette_score velocity_above cumulative_drift firing_rate waveform_duration recovery_slope spike_times spike_amplitudes waveform_mean
id
951791104 850216277 good 0.247236 90.180675 -0.343384 1.383148e-02 0.763260 0.500000 100.0 0.004499 ... 47.188066 0.104118 0.392438 418.06 2.341833 0.741709 -0.022793 [3.2095355441515783, 6.124035954291701, 17.051... [0.00010742085654993536, 0.0001035669805512930... [[0.0, -0.3348150000001908, 10.006815000000017...
951790926 850216277 noise 0.549414 96.975255 0.000000 2.362854e-01 0.676006 0.306994 140.0 0.009977 ... 25.190298 0.029867 0.171692 392.19 2.613414 1.167504 -0.032515 [2.7148354745354113, 3.1582355369324366, 3.193... [9.173462781002884e-05, 8.000884789421282e-05,... [[0.0, -5.414564999999925, 5.5407299999999395,...
951790903 850216277 good 0.219765 244.056540 -0.686767 6.284489e-02 0.867047 0.500000 60.0 0.000905 ... 30.063412 -0.036832 0.343384 483.90 0.744933 0.865327 -0.044864 [1.6110686525422746, 2.9030355010196693, 2.907... [0.00026998475697199466, 0.0002846940383350162... [[0.0, -13.258049999999884, -18.07259999999988...
951790894 850216277 good 0.164824 367.934580 -0.274707 3.215116e-05 3.188863 0.001455 70.0 0.000100 ... 72.605985 0.005048 -0.343384 71.55 0.640379 0.824121 -0.033171 [1281.742282131046, 1349.6264583506447, 1405.8... [0.00025149672984953213, 0.0004496466043690103... [[0.0, 0.71097000000006, -3.5579700000000116, ...
951790884 850216277 good 0.329648 188.990015 0.537968 7.274902e-02 0.588863 0.074423 160.0 0.000000 ... 24.436355 NaN 1.291776 0.00 0.034851 2.060302 NaN [7.834769528366286, 245.91720303227598, 438.41... [9.431834274924923e-05, 0.00010953675617822429... [[0.0, -13.237058823529424, -24.38345201238388...
951790873 850216277 noise 1.249916 392.734333 1.041597 2.236291e-07 0.446848 0.500000 140.0 0.000000 ... 94.253021 NaN -0.196219 0.00 0.004855 1.565829 NaN [488.6720371937045, 1581.39612429953, 1760.856... [0.00011043706542136272, 8.223885328261097e-05... [[0.0, 15.990000000000009, 46.18466666666663, ...
951790864 850216277 noise 1.318593 277.943055 6.364042 NaN 1.465630 0.011230 160.0 0.000000 ... NaN -0.056607 -8.328692 1810.38 0.542730 2.225126 -4.514536 [51.122208953280754, 65.47991097375385, 65.491... [0.00017703212276047242, 0.0001768216167308391... [[0.0, -1.7390099999999222, -5.484569999999962...
951790850 850216277 noise 1.648241 189.359235 -0.457845 2.436327e-01 0.850077 0.172105 160.0 0.001341 ... 16.992300 -0.070661 2.583553 999.03 0.765002 1.881742 -3.234939 [51.113175618676216, 51.12444228692837, 62.794... [0.00012096045820642862, 0.0001094972500522143... [[0.0, -0.875939999999872, -4.936424999999701,...
951790836 850216277 good 0.329648 106.165605 -0.529792 3.770473e-02 0.775795 0.500000 90.0 0.006340 ... 36.216320 0.042926 -0.892797 280.11 2.340107 0.769179 -0.020140 [0.6329351815620978, 0.6378018489136213, 1.041... [0.0001462638799394556, 0.00011085512769588882... [[0.0, 0.19090500000024235, -2.333954999999832...
951790820 850216277 good 0.178559 190.294455 -0.480737 3.121674e-02 1.488400 0.110186 70.0 0.007238 ... 36.992078 0.047044 -0.480737 232.88 1.997528 0.796650 -0.037071 [8.167536241861212, 56.18294299878077, 56.1983... [0.00017325089170459892, 0.0002418117123045365... [[0.0, 2.8191149999998117, -4.932915000000207,...

10 rows × 29 columns

nwb.intervals.keys()
dict_keys(['drifting_gratings_presentations', 'flashes_presentations', 'gabors_presentations', 'invalid_times', 'natural_movie_one_presentations', 'natural_movie_three_presentations', 'natural_scenes_presentations', 'spontaneous_presentations', 'static_gratings_presentations'])
rf_stim_table = nwb.intervals["gabors_presentations"].to_dataframe()
rf_stim_table[:10]
start_time stop_time stimulus_name stimulus_block temporal_frequency x_position y_position color mask opacity phase size units stimulus_index orientation spatial_frequency contrast tags timeseries
id
0 88.684670 88.918188 gabors 0.0 4.0 30.0 -40.0 [1.0, 1.0, 1.0] circle 1.0 [3644.93333333, 3644.93333333] [20.0, 20.0] deg 0.0 0.0 0.08 0.8 [stimulus_time_interval] [(1, 1, timestamps pynwb.base.TimeSeries at 0x...
1 88.918188 89.168385 gabors 0.0 4.0 -20.0 30.0 [1.0, 1.0, 1.0] circle 1.0 [3644.93333333, 3644.93333333] [20.0, 20.0] deg 0.0 90.0 0.08 0.8 [stimulus_time_interval] [(2, 1, timestamps pynwb.base.TimeSeries at 0x...
2 89.168385 89.418582 gabors 0.0 4.0 -20.0 20.0 [1.0, 1.0, 1.0] circle 1.0 [3644.93333333, 3644.93333333] [20.0, 20.0] deg 0.0 90.0 0.08 0.8 [stimulus_time_interval] [(3, 1, timestamps pynwb.base.TimeSeries at 0x...
3 89.418582 89.668780 gabors 0.0 4.0 -20.0 40.0 [1.0, 1.0, 1.0] circle 1.0 [3644.93333333, 3644.93333333] [20.0, 20.0] deg 0.0 0.0 0.08 0.8 [stimulus_time_interval] [(4, 1, timestamps pynwb.base.TimeSeries at 0x...
4 89.668780 89.918990 gabors 0.0 4.0 10.0 20.0 [1.0, 1.0, 1.0] circle 1.0 [3644.93333333, 3644.93333333] [20.0, 20.0] deg 0.0 90.0 0.08 0.8 [stimulus_time_interval] [(5, 1, timestamps pynwb.base.TimeSeries at 0x...
5 89.918990 90.169200 gabors 0.0 4.0 10.0 10.0 [1.0, 1.0, 1.0] circle 1.0 [3644.93333333, 3644.93333333] [20.0, 20.0] deg 0.0 45.0 0.08 0.8 [stimulus_time_interval] [(6, 1, timestamps pynwb.base.TimeSeries at 0x...
6 90.169200 90.419410 gabors 0.0 4.0 -30.0 30.0 [1.0, 1.0, 1.0] circle 1.0 [3644.93333333, 3644.93333333] [20.0, 20.0] deg 0.0 45.0 0.08 0.8 [stimulus_time_interval] [(7, 1, timestamps pynwb.base.TimeSeries at 0x...
7 90.419410 90.669620 gabors 0.0 4.0 20.0 0.0 [1.0, 1.0, 1.0] circle 1.0 [3644.93333333, 3644.93333333] [20.0, 20.0] deg 0.0 0.0 0.08 0.8 [stimulus_time_interval] [(8, 1, timestamps pynwb.base.TimeSeries at 0x...
8 90.669620 90.919822 gabors 0.0 4.0 30.0 -40.0 [1.0, 1.0, 1.0] circle 1.0 [3644.93333333, 3644.93333333] [20.0, 20.0] deg 0.0 90.0 0.08 0.8 [stimulus_time_interval] [(9, 1, timestamps pynwb.base.TimeSeries at 0x...
9 90.919822 91.170025 gabors 0.0 4.0 -10.0 40.0 [1.0, 1.0, 1.0] circle 1.0 [3644.93333333, 3644.93333333] [20.0, 20.0] deg 0.0 0.0 0.08 0.8 [stimulus_time_interval] [(10, 1, timestamps pynwb.base.TimeSeries at 0...

Selecting Units#

It would be inconvenient to try to view and generate the receptive fields for all units at once. Below you can select units based on their quality metrics and the brain region they appear in. If you’d like, redefine the select_condition function to set different criteria for selecting the units you’d like to view. For more information on the useful metrics of units in the Units table, see Visualizing Unit Quality Metrics. Because it can be very useful to select units by brain region or by probe, you may use the get_unit_location and get_unit_probe functions below, which take information from the Electrodes table to get the location and probes for each unit respectively. The sets of probe names and location names are printed for your convenience.

### helper functions made from electrodes table to get brain location of unit or probe name of unit

# map channel ids to brain location and probe group name
# creates a dictionary that iterates over indices of the dataset, assigns "id" as the key, and makes the location the value
channel_locations = {nwb.electrodes["id"][i]: nwb.electrodes["location"][i] for i in range(len(nwb.electrodes))}
# creates similar dictionary as previous line, but with the group name/probe name as the value
channel_probes = {nwb.electrodes["id"][i]: nwb.electrodes["group_name"][i] for i in range(len(nwb.electrodes))}

# function retrieves peak channel ID for given unit index from "units" dataset and looks up corresponding probe from dictionary
def get_unit_location(unit_idx):
    peak_channel_id = units["peak_channel_id"][unit_idx]
    return channel_locations[peak_channel_id]

# function retrieves peak channel ID from "units" dataset, looks up corresponding group name, returns probe associated with peak channel
def get_unit_probe(unit_idx):
    peak_channel_id = units["peak_channel_id"][unit_idx]
    return channel_probes[peak_channel_id]

print(set(channel_locations.values()))
print(set(channel_probes.values()))
{'', 'VISp', 'VIS', 'VISl', 'grey', 'VISam', 'VISrl'}
{'probeD', 'probeE', 'probeC', 'probeA', 'probeF', 'probeB'}
### select units to view the receptive fields of, change the conditional function below to select different units

def select_condition(unit_idx):
    # the values below are recommended thresholds for these quality metrics
    return units["snr"][unit_idx] > 1 and \
            units["isi_violations"][unit_idx] < 1 and \
            units["firing_rate"][unit_idx] > 0.1 and \
            get_unit_location(unit_idx) == "VISam"
            # get_unit_probe(unit_idx) == "probeC"
            # change the line above an option above to select a different brain location or probe

selected_unit_idxs = []
for unit_idx in range(len(units)):
    if select_condition(unit_idx):
        selected_unit_idxs.append(unit_idx)
        
if len(selected_unit_idxs) == 0:
    raise IndexError("There are no units for this selection")

print(len(selected_unit_idxs))
111

Getting Fields#

Below the receptive fields for each selected unit are calculated and a visualization for each one is generated. First, we get the dimensions of the receptive field plots from the set of possible coordinates of the gabor patch stimulus. Next we identify how many spikes happen in response to every stimulus for each coordinate of each unit’s receptive field. Finally, we plot each receptive field as a 2D array.

### get x and y coordinates of gabors displayed to build receptive field

xs = np.sort(list(set(rf_stim_table.x_position)))
ys = np.sort(list(set(rf_stim_table.y_position)))
field_units = rf_stim_table.units[0]
print(xs)
print(ys)
print(field_units)
[-40. -30. -20. -10.   0.  10.  20.  30.  40.]
[-40. -30. -20. -10.   0.  10.  20.  30.  40.]
deg
### get receptive field of a unit using its spike times and the stim table

def get_rf(spike_times):
    # creates 2D array that stores response spike counts for each coordinate of the receptive field
    unit_rf = np.zeros([ys.size, xs.size])
    # for every x and y coordinate in the field
    for xi, x in enumerate(xs):
        for yi, y in enumerate(ys):
            
            # for this coordinate of the rf, count all the times that this neuron responds to a stimulus time with a spike
            stim_times = rf_stim_table[(rf_stim_table.x_position == x) & (rf_stim_table.y_position == y)].start_time
            response_spike_count = 0
            for stim_time in stim_times:
                # any spike within 0.2 seconds after stim time is considered a response
                start_idx, end_idx = np.searchsorted(spike_times, [stim_time, stim_time+0.2])
                response_spike_count += end_idx-start_idx

            unit_rf[yi, xi] = response_spike_count
    
    return unit_rf
### compute receptive fields for each unit in selected units

unit_rfs = []
for unit_idx in selected_unit_idxs:
    unit_spike_times = units["spike_times"][unit_idx]
    unit_rfs.append(get_rf(unit_spike_times))
### display the receptive fields for each unit in a 2D plot

n_rows = len(unit_rfs)//10
fig, axes = plt.subplots(n_rows+1, 10)
fig.set_size_inches(12, n_rows+1)

# handle case where there's <= 10 rfs
if len(axes.shape) == 1:
    axes = axes.reshape((1, axes.shape[0]))

for irf, rf in enumerate(unit_rfs):
    ax_row = int(irf/10)
    ax_col = irf%10
    axes[ax_row][ax_col].imshow(rf, origin="lower")
for ax in axes.flat[1:]:
    ax.axis('off')

# making axis labels for first receptive field
axes[0][0].set_xlabel(field_units)
axes[0][0].set_ylabel(field_units)
axes[0][0].xaxis.set_label_position("top") 
axes[0][0].xaxis.set_ticks_position("top")
axes[0][0].set_xticks(range(len(xs)), xs, rotation=90, fontsize=6)
axes[0][0].set_yticks(range(len(ys)), ys, fontsize=6)

for i, l in enumerate(axes[0][0].xaxis.get_ticklabels()):
    if i % 2 != 0:
        l.set_visible(False)
for i, l in enumerate(axes[0][0].yaxis.get_ticklabels()):
    if i % 2 != 0:
        l.set_visible(False)
../_images/e10a3de033e1d548227918ab46c8ed2613fe972d1bb46427f6b853f9766471fa.png