Skip to content

Commit

Permalink
Merge pull request #1924 from AllenInstitute/rc/2.8.0
Browse files Browse the repository at this point in the history
rc/2.8.0
  • Loading branch information
djkapner authored Feb 26, 2021
2 parents 78ede74 + 4fd3fdb commit aa413e8
Show file tree
Hide file tree
Showing 46 changed files with 2,677 additions and 800 deletions.
4 changes: 1 addition & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,7 @@ version: 2.1
workflows:
main:
jobs:
# disable lint build, for merge to default branch
# turn back on when rc/2.8.0 is started
# - lint
- lint
- linux-py36
- linux-py37
# disabled osx build. covered by github-actions
Expand Down
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# Change Log
All notable changes to this project will be documented in this file.

## [2.7.0] = TBD
## [2.8.0] = TBD
- Created lookup table to get monitor_delay for cases where calculation from data fails
- If sync timestamp file has more timestamps than eye tracking moving has frame, trim excess timestamps (up to 15)
- Session API returns both warped and unwarped stimulus images, and both are written to NWB

## [2.7.0] = 2021-02-19
- Refactored behavior and ophys session and data APIs to remove a circular inheritance issue
- Fixed segmentation mask and roi_mask misregistration in 'BehaviorOphysSession'
- Replaces BehaviorOphysSession.get_roi_masks() method with roi_masks property
Expand Down
11 changes: 5 additions & 6 deletions allensdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@
#
import logging




__version__ = '2.7.0'
__version__ = '2.8.0'


try:
Expand All @@ -61,7 +58,8 @@ def one(x):
except TypeError:
return x
if xlen != 1:
raise OneResultExpectedError('Expected length one result, received: {} results from query'.format(x))
raise OneResultExpectedError("Expected length one result, received: "
f"{x} results from queryr")
if isinstance(x, set):
return list(x)[0]
else:
Expand All @@ -75,6 +73,7 @@ def one(x):
'allensdk.api.api.retrieve_file_over_http')
file_download_log.setLevel(logging.INFO)
console = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
formatter = logging.Formatter("%(asctime)s %(name)-12s "
"%(levelname)-8s %(message)s")
console.setFormatter(formatter)
file_download_log.addHandler(console)
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def events(self) -> pd.DataFrame:

@events.setter
def events(self, value):
self_events = value
self._events = value

@property
def cell_specimen_table(self) -> pd.DataFrame:
Expand Down
21 changes: 7 additions & 14 deletions allensdk/brain_observatory/behavior/behavior_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def get_performance_metrics(self, engaged_trial_reward_rate_threshold=2):
performance_metrics['total_reward_count'] = len(self.rewards)
performance_metrics['total_reward_volume'] = self.rewards.volume.sum()

rpdf = self.get_rpdf()
rpdf = self.get_rolling_performance_df()
engaged_trial_mask = (
rpdf['reward_rate'] >
engaged_trial_reward_rate_threshold)
Expand Down Expand Up @@ -313,24 +313,17 @@ def stimulus_presentations(self, value):
self._stimulus_presentations = value

@property
def stimulus_templates(self) -> StimulusTemplate:
def stimulus_templates(self) -> pd.DataFrame:
"""Get stimulus templates (movies, scenes) for behavior session.
Returns
-------
StimulusTemplate
A StimulusTemplate object containing the stimulus images for the
experiment. Relevant properties include:
image_set_name: The name of the image set that the
StimulusTemplate encapsulates
image_names: A list of individual image names in the image set
images: A list of StimulusImage (inherits from np.ndarray)
objects.
Also has a to_dataframe() method to convert to a dataframe
where indices are image names, an 'image' column contains image
arrays, and the df.name is the image set.
pd.DataFrame
A pandas DataFrame object containing the stimulus images for the
experiment. Indices are image names, 'warped' and 'unwarped'
columns contains image arrays, and the df.name is the image set.
"""
return self._stimulus_templates
return self._stimulus_templates.to_dataframe()

@stimulus_templates.setter
def stimulus_templates(self, value):
Expand Down
22 changes: 18 additions & 4 deletions allensdk/brain_observatory/behavior/eye_tracking_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def load_eye_tracking_hdf(eye_tracking_file: Path) -> pd.DataFrame:

# Values in the hdf5 may be complex (likely an artifact of the ellipse
# fitting process). Take only the real component.
eye_tracking_data = eye_tracking_data.apply(lambda x: np.real(x.to_numpy()))
eye_tracking_data = eye_tracking_data.apply(lambda x: np.real(x.to_numpy())) # noqa: E501

return eye_tracking_data.astype(float)

Expand Down Expand Up @@ -186,9 +186,23 @@ def process_eye_tracking_data(eye_data: pd.DataFrame,
eye tracking frames.
"""

if len(frame_times) != len(eye_data.index):
n_sync = len(frame_times)
n_eye_frames = len(eye_data.index)

# If n_sync exceeds n_eye_frames by <= 15,
# just trim the excess sync pulses from the end
# of the timestamps array.
#
# This solution was discussed in
# https://github.com/AllenInstitute/AllenSDK/issues/1545

if n_sync > n_eye_frames and n_sync <= n_eye_frames+15:
frame_times = frame_times[:n_eye_frames]
n_sync = len(frame_times)

if n_sync != n_eye_frames:
raise RuntimeError(f"Error! The number of sync file frame times "
f"({len(frame_times)} does not match the "
f"({len(frame_times)}) does not match the "
f"number of eye tracking frames "
f"({len(eye_data.index)})!")

Expand Down Expand Up @@ -217,7 +231,7 @@ def process_eye_tracking_data(eye_data: pd.DataFrame,
cr_areas[likely_blinks] = np.nan
eye_areas[likely_blinks] = np.nan

eye_data.insert(0, "time", frame_times)
eye_data.insert(0, "timestamps", frame_times)
eye_data.insert(1, "cr_area", cr_areas)
eye_data.insert(2, "eye_area", eye_areas)
eye_data.insert(3, "pupil_area", pupil_areas)
Expand Down
81 changes: 70 additions & 11 deletions allensdk/brain_observatory/behavior/metadata_processing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Dict
import re
import numpy as np

description_dict = {
# key is a regex and value is returned on match
Expand Down Expand Up @@ -55,25 +57,82 @@ def get_expt_description(session_type: str) -> str:
return match.popitem()[1]


def get_task_parameters(data):
def get_task_parameters(data: Dict) -> Dict:
"""
Read task_parameters metadata from the behavior stimulus pickle file.
Parameters
----------
data: dict
The nested dict read in from the behavior stimulus pickle file.
All of the data expected by this method lives under
data['items']['behavior']
Returns
-------
dict
A dict containing the task_parameters associated with this session.
"""
behavior = data["items"]["behavior"]
stimuli = behavior['stimuli']
config = behavior["config"]
doc = config["DoC"]

task_parameters = {}

task_parameters['blank_duration_sec'] = \
[float(x) for x in behavior['config']['DoC']['blank_duration_range']]
task_parameters['stimulus_duration_sec'] = \
behavior['config']['DoC']['stimulus_window']
[float(x) for x in doc['blank_duration_range']]

if 'images' in stimuli:
stim_key = 'images'
elif 'grating' in stimuli:
stim_key = 'grating'
else:
msg = "Cannot get stimulus_duration_sec\n"
msg += "'images' and/or 'grating' not a valid "
msg += "key in pickle file under "
msg += "['items']['behavior']['stimuli']\n"
msg += f"keys: {list(stimuli.keys())}"
raise RuntimeError(msg)

stim_duration = stimuli[stim_key]['flash_interval_sec']

# from discussion in
# https://github.com/AllenInstitute/AllenSDK/issues/1572
#
# 'flash_interval' contains (stimulus_duration, gray_screen_duration)
# (as @matchings said above). That second value is redundant with
# 'blank_duration_range'. I'm not sure what would happen if they were
# set to be conflicting values in the params. But it looks like
# they're always consistent. It should always be (0.25, 0.5),
# except for TRAINING_0 and TRAINING_1, which have statically
# displayed stimuli (no flashes).

if stim_duration is None:
stim_duration = np.NaN
else:
stim_duration = stim_duration[0]

task_parameters['stimulus_duration_sec'] = stim_duration

task_parameters['omitted_flash_fraction'] = \
behavior['params'].get('flash_omit_probability', float('nan'))
task_parameters['response_window_sec'] = \
[float(x) for x in behavior["config"]["DoC"]["response_window"]]
task_parameters['reward_volume'] = \
behavior["config"]["reward"]["reward_volume"]
task_parameters['stage'] = behavior["params"]["stage"]
[float(x) for x in doc["response_window"]]
task_parameters['reward_volume'] = config["reward"]["reward_volume"]
task_parameters['auto_reward_volume'] = doc['auto_reward_volume']
task_parameters['session_type'] = behavior["params"]["stage"]
task_parameters['stimulus'] = next(iter(behavior["stimuli"]))
task_parameters['stimulus_distribution'] = \
behavior["config"]["DoC"]["change_time_dist"]
task_parameters['task'] = behavior["config"]["behavior"]["task_id"]
task_parameters['stimulus_distribution'] = doc["change_time_dist"]

task_id = config['behavior']['task_id']
if 'DoC' in task_id:
task_parameters['task'] = 'change detection'
else:
msg = "metadata_processing.get_task_parameters does not "
msg += f"know how to parse 'task_id' = {task_id}"
raise RuntimeError(msg)

n_stimulus_frames = 0
for stim_type, stim_table in behavior["stimuli"].items():
n_stimulus_frames += sum(stim_table.get("draw_log", []))
Expand Down
6 changes: 3 additions & 3 deletions allensdk/brain_observatory/behavior/rewards_processing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from typing import Dict
import numpy as np
import pandas as pd
from collections import defaultdict


def get_rewards(data: Dict,
Expand Down Expand Up @@ -31,13 +30,14 @@ def get_rewards(data: Dict,
trial_df = pd.DataFrame(data["items"]["behavior"]["trial_log"])
rewards_dict = {"volume": [], "timestamps": [], "autorewarded": []}
for idx, trial in trial_df.iterrows():
rewards = trial["rewards"] # as i write this there can only ever be one reward per trial
rewards = trial["rewards"]
# as i write this there can only ever be one reward per trial
if rewards:
rewards_dict["volume"].append(rewards[0][0])
rewards_dict["timestamps"].append(timestamps[rewards[0][2]])
auto_rwrd = trial["trial_params"]["auto_reward"]
rewards_dict["autorewarded"].append(auto_rwrd)

df = pd.DataFrame(rewards_dict).set_index("timestamps", drop=True)
df = pd.DataFrame(rewards_dict)

return df
7 changes: 6 additions & 1 deletion allensdk/brain_observatory/behavior/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ class BehaviorTaskParametersSchema(RaisingSchema):
stimulus_duration_sec = fields.Float(
doc='Duration of each stimulus presentation in seconds',
required=True,
allow_nan=True
)
omitted_flash_fraction = fields.Float(
doc='Fraction of flashes/image presentations that were omitted',
Expand All @@ -232,7 +233,11 @@ class BehaviorTaskParametersSchema(RaisingSchema):
doc='Volume of water (in mL) delivered as reward',
required=True,
)
stage = fields.String(
auto_reward_volume = fields.Float(
doc='Volume of water (in mL) delivered as an automatic reward',
required=True,
)
session_type = fields.String(
doc='Stage of behavioral task',
required=True,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from allensdk.brain_observatory.behavior.schemas import (
BehaviorTaskParametersSchema, OphysBehaviorMetadataSchema)
from allensdk.brain_observatory.behavior.stimulus_processing import \
StimulusTemplate
StimulusTemplate, StimulusTemplateFactory
from allensdk.brain_observatory.behavior.trials_processing import (
TRIAL_COLUMN_DESCRIPTION_DICT
)
Expand Down Expand Up @@ -75,12 +75,14 @@ def save(self, session_object):
from_dataframe=True)

# Add stimulus template data to NWB in-memory object:
# Not all sessions will have stimulus_templates (e.g. gratings)
if session_object.stimulus_templates:
self._add_stimulus_templates(
nwbfile=nwbfile,
stimulus_templates=session_object.stimulus_templates,
stimulus_presentations=session_object.stimulus_presentations)
# Use the semi-private _stimulus_templates attribute because it is
# a StimulusTemplate object. The public stimulus_templates property
# of the session_object returns a DataFrame.
session_stimulus_templates = session_object._stimulus_templates
self._add_stimulus_templates(
nwbfile=nwbfile,
stimulus_templates=session_stimulus_templates,
stimulus_presentations=session_object.stimulus_presentations)

# search for omitted rows and add stop_time before writing to NWB file
set_omitted_stop_time(
Expand Down Expand Up @@ -200,12 +202,10 @@ def get_stimulus_templates(self, **kwargs) -> Optional[StimulusTemplate]:

image_attributes = [{'image_name': image_name}
for image_name in image_data.control_description]
stimulus_templates = StimulusTemplate(
image_set_name=image_set_name,
image_attributes=image_attributes,
images=image_data.data[:]
return StimulusTemplateFactory.from_processed(
image_set_name=image_set_name, image_attributes=image_attributes,
warped=image_data.data[:], unwarped=image_data.unwarped[:]
)
return stimulus_templates

def get_stimulus_timestamps(self) -> np.ndarray:
stim_module = self.nwbfile.processing['stimulus']
Expand All @@ -224,7 +224,7 @@ def get_licks(self) -> np.ndarray:
licks = lick_module.get_data_interface('licks')

return pd.DataFrame({
'time': licks.timestamps[:],
'timestamps': licks.timestamps[:],
'frame': licks.data[:]
})
else:
Expand All @@ -238,11 +238,11 @@ def get_rewards(self) -> np.ndarray:
volume = rewards.get_data_interface('volume').data[:]
return pd.DataFrame({
'volume': volume, 'timestamps': time,
'autorewarded': autorewarded}).set_index('timestamps')
'autorewarded': autorewarded})
else:
return pd.DataFrame({
'volume': [], 'timestamps': [],
'autorewarded': []}).set_index('timestamps')
'autorewarded': []})

def get_metadata(self) -> dict:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ class BehaviorOphysJsonApi(BehaviorOphysDataTransforms):
a specified raw data source (extractor). Contains all methods
needed to fill a BehaviorOphysSession."""

def __init__(self, data):
def __init__(self, data: dict, skip_eye_tracking: bool = False):
extractor = BehaviorOphysJsonExtractor(data=data)
super().__init__(extractor=extractor)
super().__init__(extractor=extractor,
skip_eye_tracking=skip_eye_tracking)


class BehaviorOphysJsonExtractor(BehaviorJsonExtractor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def __init__(self,
ophys_experiment_id: Optional[int] = None,
lims_credentials: Optional[DbCredentials] = None,
mtrain_credentials: Optional[DbCredentials] = None,
extractor: Optional[BehaviorOphysDataExtractorBase] = None):
extractor: Optional[BehaviorOphysDataExtractorBase] = None,
skip_eye_tracking: bool = False):

if extractor is None:
if ophys_experiment_id is not None:
Expand All @@ -40,7 +41,8 @@ def __init__(self,
"BehaviorOphysLimsApi must be provided either an "
"instantiated 'extractor' or an 'ophys_experiment_id'!")

super().__init__(extractor=extractor)
super().__init__(extractor=extractor,
skip_eye_tracking=skip_eye_tracking)


class BehaviorOphysLimsExtractor(OphysLimsExtractor, BehaviorLimsExtractor,
Expand Down
Loading

0 comments on commit aa413e8

Please sign in to comment.