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 inlineDownload 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()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]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'])stim_name = "gabors_presentations"
# stim_name = "RF mapping_presentations"
rf_stim_table = nwb.intervals[stim_name].to_dataframe()
rf_stim_table[:10]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
# In older Allen NWBs the information must be manually cross-referenced between the units table and electrodes table
# 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 location 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]
# In newer Allen NWBs, the units table includes direct references to each unit's electrodes, so we can access them directly
# def get_peak_channel_idx(unit_idx):
# mean_waveforms = units["waveform_mean"][unit_idx]
# waveform_mins = np.min(mean_waveforms, axis=0)
# peak_channel_idx = np.argmin(waveform_mins)
# return peak_channel_idx
# def get_unit_location(unit_idx):
# peak_channel_idx = get_peak_channel_idx(unit_idx)
# detected_electrodes = units["electrodes"][unit_idx]
# return detected_electrodes.iloc[peak_channel_idx].location
# def get_unit_probe(unit_idx):
# peak_channel_idx = get_peak_channel_idx(unit_idx)
# detected_electrodes = units["electrodes"][unit_idx]
# return detected_electrodes.iloc[peak_channel_idx].group_name
print(set(channel_locations.values()))
print(set(channel_probes.values())){'', 'grey', 'VISam', 'VISrl', 'VISp', 'VISl', 'VIS'}
{'probeC', 'probeA', 'probeD', 'probeB', 'probeF', 'probeE'}
### select units to view the receptive fields of, change the conditional function below to select different units
isi_column = "isi_violations_count" if "isi_violations_count" in units.colnames else "isi_violations"
def select_condition(unit_idx):
# the values below are recommended thresholds for these quality metrics
return units["snr"][unit_idx] > 1 and \
units[isi_column][unit_idx] < 1 and \
units["firing_rate"][unit_idx] > 0.1
# 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))1255
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
for coord_key in ('pos_x','x_position','x_pos','X'):
if coord_key in rf_stim_table.columns:
x_key = coord_key
y_key = coord_key.replace('x','y').replace('X','Y')
break
else:
raise ValueError("Could not find x and y columns in RF intervals table")
xs = np.sort(list(set(rf_stim_table[x_key])))
ys = np.sort(list(set(rf_stim_table[y_key])))
field_units = rf_stim_table.get("units", ["unknown"])[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
def get_rf_vectorized(spike_times, rf_stim_table, xs=None, ys=None, response_window=0.2):
"""
Calculate receptive field using vectorized operations for improved performance.
Parameters:
-----------
spike_times : array-like
Spike times for the unit
rf_stim_table : pandas.DataFrame
Stimulus table containing gabor presentations
xs : array-like, optional
X-coordinates of the receptive field. If not provided, uses rf_stim_table.x_positions
ys : array-like, optional
Y-coordinates of the receptive field. If not provided, uses rf_stim_table.y_positions
response_window : float, optional
Time window after stimulus onset to count spikes (default: 0.2 seconds)
Returns:
--------
unit_rf : numpy.ndarray
2D array containing spike counts for each position in the receptive field
"""
if xs is None:
xs = np.sort(rf_stim_table[x_key].unique())
if ys is None:
ys = np.sort(rf_stim_table[y_key].unique())
# Initialize the receptive field array
unit_rf = np.full((len(ys), len(xs)), np.nan)
# Pre-compute stimulus presentations by position
position_stim_times = {}
for xi, x in enumerate(xs):
for yi, y in enumerate(ys):
position_key = (x, y)
position_stim_times[position_key] = rf_stim_table[
(rf_stim_table[x_key] == x) &
(rf_stim_table[y_key] == y)
].start_time.values
# Process each position
for xi, x in enumerate(xs):
for yi, y in enumerate(ys):
position_key = (x, y)
stim_times = position_stim_times[position_key]
if len(stim_times) == 0:
continue
# Create arrays of start and end times for the response windows
start_times = stim_times
end_times = stim_times + response_window
# Find indices for all spikes within the response windows in one operation
# This is much faster than searching for each stimulus individually
all_indices = np.searchsorted(spike_times, np.column_stack((start_times, end_times)))
# Calculate spike counts for all presentations at once
spike_counts = all_indices[:, 1] - all_indices[:, 0]
# Store the total spike count for this position
unit_rf[yi, xi] = np.mean(spike_counts)
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_vectorized(unit_spike_times, rf_stim_table))### 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)