Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from allensdk.core.lazy_property import LazyProperty
from allensdk.brain_observatory.nwb.nwb_api import NwbApi
from allensdk.brain_observatory.nwb.nwb_utils import set_omitted_stop_time
from allensdk.brain_observatory.behavior.trials_processing import TRIAL_COLUMN_DESCRIPTION_DICT
from allensdk.brain_observatory.behavior.schemas import OphysBehaviorMetaDataSchema, OphysBehaviorTaskParametersSchema
from allensdk.brain_observatory.nwb.metadata import load_LabMetaData_extension
Expand Down Expand Up @@ -59,6 +60,9 @@ def save(self, session_object):
stimulus_index = session_object.stimulus_presentations[session_object.stimulus_presentations['image_set'] == nwb_template.name]
nwb.add_stimulus_index(nwbfile, stimulus_index, nwb_template)

# search for omitted rows and add stop_time before writing to NWB file
set_omitted_stop_time(stimulus_table=session_object.stimulus_presentations)

# Add stimulus presentations data to NWB in-memory object:
nwb.add_stimulus_presentations(nwbfile, session_object.stimulus_presentations)

Expand Down
30 changes: 27 additions & 3 deletions allensdk/brain_observatory/behavior/stimulus_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,17 @@ def load_pickle(pstream):
return pickle.load(pstream, encoding="bytes")


def get_stimulus_presentations(data, stimulus_timestamps):

def get_stimulus_presentations(data, stimulus_timestamps) -> pd.DataFrame:
"""
This function retrieves the stimulus presentation dataframe and
renames the columns, adds a stop_time column, and set's index to
stimulus_presentation_id before sorting and returning the dataframe.
:param data: stimulus file associated with experiment id
:param stimulus_timestamps: timestamps indicating when stimuli switched
during experiment
:return: stimulus_table: dataframe containing the stimuli metadata as well
as what stimuli was presented
"""
stimulus_table = get_visual_stimuli_df(data, stimulus_timestamps)
# workaround to rename columns to harmonize with visual coding and rebase timestamps to sync time
stimulus_table.insert(loc=0, column='flash_number', value=np.arange(0, len(stimulus_table)))
Expand Down Expand Up @@ -159,7 +168,22 @@ def unpack_change_log(change):
to_name=to_name,
)

def get_visual_stimuli_df(data, time):

def get_visual_stimuli_df(data, time) -> pd.DataFrame:
"""
This function loads the stimuli and the omitted stimuli into a dataframe.
These stimuli are loaded from the input data, where the set_log and
draw_log contained within are used to calculate the epochs. These epochs
are used as start_frame and end_frame and converted to times by input
stimulus timestamps. The omitted stimuli do not have a end_frame by design
though there duration is always 250ms.
:param data: the behavior data file
:param time: the stimulus timestamps indicating when each stimuli is
displayed
:return: df: a pandas dataframe containing the stimuli and omitted stimuli
that were displayed with their frame, end_frame, start_time,
and duration
"""

stimuli = data['items']['behavior']['stimuli']
n_frames = len(time)
Expand Down
9 changes: 7 additions & 2 deletions allensdk/brain_observatory/nwb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from pynwb.ophys import DfOverF, ImageSegmentation, OpticalChannel, Fluorescence

import allensdk.brain_observatory.roi_masks as roi
from allensdk.brain_observatory.nwb.nwb_utils import (get_column_name)
from allensdk.brain_observatory.running_speed import RunningSpeed
from allensdk.brain_observatory import dict_to_indexed_array
from allensdk.brain_observatory.behavior.image_api import Image
Expand Down Expand Up @@ -424,10 +425,13 @@ def add_stimulus_presentations(nwbfile, stimulus_table, tag='stimulus_time_inter
"""
stimulus_table = stimulus_table.copy()
ts = nwbfile.modules['stimulus'].get_data_interface('timestamps')
stimulus_names = stimulus_table['stimulus_name'].unique()
possible_names = {'stimulus_name', 'image_name'}
stimulus_name_column = get_column_name(stimulus_table.columns,
possible_names)
stimulus_names = stimulus_table[stimulus_name_column].unique()

for stim_name in sorted(stimulus_names):
specific_stimulus_table = stimulus_table[stimulus_table['stimulus_name'] == stim_name]
specific_stimulus_table = stimulus_table[stimulus_table[stimulus_name_column] == stim_name]
# Drop columns where all values in column are NaN
cleaned_table = specific_stimulus_table.dropna(axis=1, how='all')
# For columns with mixed strings and NaNs, fill NaNs with 'N/A'
Expand All @@ -447,6 +451,7 @@ def add_stimulus_presentations(nwbfile, stimulus_table, tag='stimulus_time_inter

for row in cleaned_table.itertuples(index=False):
row = row._asdict()

presentation_interval.add_interval(**row, tags=tag, timeseries=ts)

nwbfile.add_time_intervals(presentation_interval)
Expand Down
52 changes: 52 additions & 0 deletions allensdk/brain_observatory/nwb/nwb_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import pandas as pd
# All of the omitted stimuli have a duration of 250ms as defined
# by the Visual Behavior team. For questions about duration contact that
# team.


def get_column_name(table_cols: list,
possible_names: set) -> str:
"""
This function acts a identifier for which column name is present in the
dataframe from the provided possibilities. This is used in NWB to identify
the correct column name for stimulus_name which differs from Behavior Ophys
to Eccephys.
:param table_cols: the table columns to search for the possible name within
:param possible_names: the names that could exist within the data columns
:return: the first entry of the intersection between the possible names
and the names of the columns of the stimulus table
"""

column_set = set(table_cols)
column_names = list(column_set.intersection(possible_names))
if not len(column_names) == 1:
raise KeyError("Table expected one name column in intersection, found:"
f" {column_names}")
return column_names[0]


def set_omitted_stop_time(stimulus_table: pd.DataFrame,
omitted_time_duration: float=0.25) -> None:
"""
This function sets the stop time for a row that of a stimuli table that
is a omitted stimuli. A omitted stimuli is a stimuli where a mouse is
shown only a grey screen and these last for 250 milliseconds. These do not
include a stop_time or end_frame as other stimuli in the stimulus table due
to design choices. For these stimuli to be added they must have the
stop_time calculated and put into the row as data before writing to NWB.
:param stimulus_table: pd.DataFrame that contains the stimuli presented to
an experiment subject
:param omitted_time_duration: The duration in seconds of the expected length
of the omitted stimuli
:return:
stimulus_table_row: returns the same dictionary as inputted but with
an additional entry for stop_time.
"""
omitted_row_indexs = stimulus_table.index[stimulus_table['omitted']].tolist()
for omitted_row_idx in omitted_row_indexs:
row = stimulus_table.iloc[omitted_row_idx]
start_time = row['start_time']
end_time = start_time + omitted_time_duration
row['stop_time'] = end_time
row['duration'] = omitted_time_duration
stimulus_table.iloc[omitted_row_idx] = row
58 changes: 58 additions & 0 deletions allensdk/test/brain_observatory/nwb/test_nwb_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import pytest
import pandas as pd

from allensdk.brain_observatory.nwb import nwb_utils


@pytest.mark.parametrize("input_cols, possible_names, expected_intersection", [
(['duration', 'end_frame', 'image_index', 'image_name'],
{'stimulus_name', 'image_name'}, 'image_name'),
(['duration', 'end_frame', 'image_index', 'stimulus_name'],
{'stimulus_name', 'image_name'}, 'stimulus_name')
])
def test_get_stimulus_name_column(input_cols, possible_names,
expected_intersection):
column_name = nwb_utils.get_column_name(input_cols, possible_names)
assert column_name == expected_intersection


@pytest.mark.parametrize("input_cols, possible_names, expected_excep_cols", [
(['duration', 'end_frame', 'image_index'], {'stimulus_name', 'image_name'},
[]),
(['duration', 'end_frame', 'image_index', 'image_name', 'stimulus_name'],
{'stimulus_name', 'image_name'},
['stimulus_name', 'image_name'])
])
def test_get_stimulus_name_column_exceptions(input_cols,
possible_names,
expected_excep_cols):
with pytest.raises(KeyError) as error:
nwb_utils.get_column_name(input_cols, possible_names)
for expected_value in expected_excep_cols:
assert expected_value in str(error.value)


@pytest.mark.parametrize("stimulus_table, expected_table_data", [
({'image_index': [8, 9],
'image_name': ['omitted', 'not_omitted'],
'image_set': ['omitted', 'not_omitted'],
'index': [201, 202],
'omitted': [True, False],
'start_frame': [231060, 232340],
'start_time': [0, 250],
'stop_time': [None, 1340509]},
{'image_index': [8, 9],
'image_name': ['omitted', 'not_omitted'],
'image_set': ['omitted', 'not_omitted'],
'index': [201, 202],
'omitted': [True, False],
'start_frame': [231060, 232340],
'start_time': [0, 250],
'stop_time': [0.25, 1340509]}
)
])
def test_set_omitted_stop_time(stimulus_table, expected_table_data):
stimulus_table = pd.DataFrame.from_dict(data=stimulus_table)
expected_table = pd.DataFrame.from_dict(data=expected_table_data)
nwb_utils.set_omitted_stop_time(stimulus_table)
assert stimulus_table.equals(expected_table)