import numpy as np
import pandas as pd
import logging
import warnings
from .stimulus_analysis import StimulusAnalysis
warnings.simplefilter(action='ignore', category=FutureWarning)
logger = logging.getLogger(__name__)
[docs]class NaturalScenes(StimulusAnalysis):
"""
A class for computing single-unit metrics from the natural scenes stimulus of an ecephys session NWB file.
To use, pass in a EcephysSession object::
session = EcephysSession.from_nwb_path('/path/to/my.nwb')
ns_analysis = NaturalScenes(session)
or, alternatively, pass in the file path::
ns_analysis = NaturalScenes('/path/to/my.nwb')
You can also pass in a unit filter dictionary which will only select units with certain properties. For example
to get only those units which are on probe C and found in the VISp area::
ns_analysis = NaturalScenes(session, filter={'location': 'probeC', 'ecephys_structure_acronym': 'VISp'})
To get a table of the individual unit metrics ranked by unit ID::
metrics_table_df = ns_analysis.metrics()
"""
def __init__(self, ecephys_session, col_image='frame', trial_duration=0.25, **kwargs):
super(NaturalScenes, self).__init__(ecephys_session, trial_duration=trial_duration, **kwargs)
self._images = None
self._number_images = None
self._images_nonblank = None
self._number_nonblank = None # does not include Image number = -1.
self._mean_sweep_events = None
self._response_events = None
self._response_trials = None
self._metrics = None
self._col_image = col_image
if self._params is not None:
self._params = self._params.get('natural_scenes', {})
self._stimulus_key = self._params.get('stimulus_key', None) # Overwrites parent value with argvars
else:
self._params = {}
@property
def name(self):
return 'Natural Scenes'
@property
def images(self):
""" Array of iamge labels """
if self._images is None:
self._get_stim_table_stats()
return self._images
@property
def images_nonblank(self):
if self._images_nonblank is None:
self._get_stim_table_stats()
return self._images_nonblank
@property
def frames(self):
# Required to deal with naming difference between NWB 1 and 2
return self.images
@property
def number_images(self):
""" Number of images shown """
if self._images is None:
self._get_stim_table_stats()
return self._number_images
@property
def number_nonblank(self):
""" Number of images shown (excluding blank condition) """
if self._number_nonblank is None:
self._get_stim_table_stats()
return self._number_nonblank
@property
def null_condition(self):
""" Stimulus condition ID for null (blank) stimulus """
return self.stimulus_conditions[self.stimulus_conditions[self._col_image] == -1].index
@property
def METRICS_COLUMNS(self):
return [('pref_image_ns', np.uint64),
('image_selectivity_ns', np.float64),
('firing_rate_ns', np.float64),
('fano_ns', np.float64),
('time_to_peak_ns', np.float64),
('lifetime_sparseness_ns', np.float64),
('run_pval_ns', np.float64),
('run_mod_ns', np.float64)]
@property
def metrics(self):
if self._metrics is None:
logger.info('Calculating metrics for ' + self.name)
unit_ids = self.unit_ids
metrics_df = self.empty_metrics_table()
if len(self.stim_table) > 0:
logger.info('Calculating metrics for ' + self.name)
metrics_df['pref_image_ns'] = [self._get_preferred_condition(unit) for unit in unit_ids]
metrics_df['pref_images_multi_ns'] = [
self._check_multiple_pref_conditions(unit_id, self._col_image, self.images_nonblank)
for unit_id in unit_ids
]
metrics_df['image_selectivity_ns'] = [self._get_image_selectivity(unit) for unit in unit_ids]
metrics_df['firing_rate_ns'] = [self._get_overall_firing_rate(unit) for unit in unit_ids]
metrics_df['fano_ns'] = [self._get_fano_factor(unit, self._get_preferred_condition(unit))
for unit in unit_ids]
metrics_df['time_to_peak_ns'] = [self._get_time_to_peak(unit, self._get_preferred_condition(unit))
for unit in unit_ids]
metrics_df['lifetime_sparseness_ns'] = [self._get_lifetime_sparseness(unit) for unit in unit_ids]
metrics_df.loc[:, ['run_pval_ns', 'run_mod_ns']] = [
self._get_running_modulation(unit, self._get_preferred_condition(unit)) for unit in unit_ids]
self._metrics = metrics_df
return self._metrics
[docs] @classmethod
def known_stimulus_keys(cls):
return ['natural_scenes', 'Natural_Images', 'Natural Images']
def _get_stim_table_stats(self):
""" Extract image labels from the stimulus table """
self._images = np.sort(self.stimulus_conditions[self._col_image].unique()).astype(np.int64)
self._number_images = len(self._images)
self._images_nonblank = self._images[self._images >= 0]
self._number_nonblank = len(self._images_nonblank)
def _get_image_selectivity(self, unit_id, num_steps=1000):
""" Calculate the image selectivity for a given unit using spike means at every image"""
unit_stats = self.conditionwise_statistics.loc[unit_id].drop(index=self.null_condition)
return image_selectivity(unit_stats['spike_mean'].values, num_steps=num_steps)
[docs]def image_selectivity(spike_means, num_steps=1000):
"""Quantifies how selective a cell is for images, based on Quian Quiroga et al., 2007. A value of 0 indicates
the cell responds the same no mater what the image. While if the neuron only responds to a single image it
will have a selectivity of 1 - 2/N (1.0 and N goes to inf).
Parameters
----------
spike_means : array of floats
Averaged spiking responses to a series of images for a given neuron
num_steps : int
Number of threshold values used to build response distribution (default to 1000 as in Quian paper)
Returns
-------
selectivity : float
selectivity of neuron to images
"""
if spike_means.size < 2 or num_steps < 2:
# What is the selectivity of none of 0 spikes (by definition should be 0 and 1)
return np.nan
# Essentially creates a cumulative distribution function of responses at a given set of thresholds, finds the
# area under the response distribution and normalizes between 0 and 1.
fmin = spike_means.min()
fmax = spike_means.max()
if fmin == fmax:
# A uniform response of none for each image, make sure to return 0
return 0.0
j = np.arange(num_steps)
thresh = fmin + j*((fmax - fmin) / num_steps)
rtj = [np.mean(spike_means > t) for t in thresh]
return 1 - (2 * np.mean(rtj))