Skip to content

Commit aa413e8

Browse files
authored
Merge pull request #1924 from AllenInstitute/rc/2.8.0
rc/2.8.0
2 parents 78ede74 + 4fd3fdb commit aa413e8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2677
-800
lines changed

.circleci/config.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,7 @@ version: 2.1
105105
workflows:
106106
main:
107107
jobs:
108-
# disable lint build, for merge to default branch
109-
# turn back on when rc/2.8.0 is started
110-
# - lint
108+
- lint
111109
- linux-py36
112110
- linux-py37
113111
# disabled osx build. covered by github-actions

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
# Change Log
22
All notable changes to this project will be documented in this file.
33

4-
## [2.7.0] = TBD
4+
## [2.8.0] = TBD
5+
- Created lookup table to get monitor_delay for cases where calculation from data fails
6+
- If sync timestamp file has more timestamps than eye tracking moving has frame, trim excess timestamps (up to 15)
7+
- Session API returns both warped and unwarped stimulus images, and both are written to NWB
8+
9+
## [2.7.0] = 2021-02-19
510
- Refactored behavior and ophys session and data APIs to remove a circular inheritance issue
611
- Fixed segmentation mask and roi_mask misregistration in 'BehaviorOphysSession'
712
- Replaces BehaviorOphysSession.get_roi_masks() method with roi_masks property

allensdk/__init__.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,7 @@
3535
#
3636
import logging
3737

38-
39-
40-
41-
__version__ = '2.7.0'
38+
__version__ = '2.8.0'
4239

4340

4441
try:
@@ -61,7 +58,8 @@ def one(x):
6158
except TypeError:
6259
return x
6360
if xlen != 1:
64-
raise OneResultExpectedError('Expected length one result, received: {} results from query'.format(x))
61+
raise OneResultExpectedError("Expected length one result, received: "
62+
f"{x} results from queryr")
6563
if isinstance(x, set):
6664
return list(x)[0]
6765
else:
@@ -75,6 +73,7 @@ def one(x):
7573
'allensdk.api.api.retrieve_file_over_http')
7674
file_download_log.setLevel(logging.INFO)
7775
console = logging.StreamHandler()
78-
formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
76+
formatter = logging.Formatter("%(asctime)s %(name)-12s "
77+
"%(levelname)-8s %(message)s")
7978
console.setFormatter(formatter)
8079
file_download_log.addHandler(console)

allensdk/brain_observatory/behavior/behavior_ophys_session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ def events(self) -> pd.DataFrame:
234234

235235
@events.setter
236236
def events(self, value):
237-
self_events = value
237+
self._events = value
238238

239239
@property
240240
def cell_specimen_table(self) -> pd.DataFrame:

allensdk/brain_observatory/behavior/behavior_session.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def get_performance_metrics(self, engaged_trial_reward_rate_threshold=2):
174174
performance_metrics['total_reward_count'] = len(self.rewards)
175175
performance_metrics['total_reward_volume'] = self.rewards.volume.sum()
176176

177-
rpdf = self.get_rpdf()
177+
rpdf = self.get_rolling_performance_df()
178178
engaged_trial_mask = (
179179
rpdf['reward_rate'] >
180180
engaged_trial_reward_rate_threshold)
@@ -313,24 +313,17 @@ def stimulus_presentations(self, value):
313313
self._stimulus_presentations = value
314314

315315
@property
316-
def stimulus_templates(self) -> StimulusTemplate:
316+
def stimulus_templates(self) -> pd.DataFrame:
317317
"""Get stimulus templates (movies, scenes) for behavior session.
318318
319319
Returns
320320
-------
321-
StimulusTemplate
322-
A StimulusTemplate object containing the stimulus images for the
323-
experiment. Relevant properties include:
324-
image_set_name: The name of the image set that the
325-
StimulusTemplate encapsulates
326-
image_names: A list of individual image names in the image set
327-
images: A list of StimulusImage (inherits from np.ndarray)
328-
objects.
329-
Also has a to_dataframe() method to convert to a dataframe
330-
where indices are image names, an 'image' column contains image
331-
arrays, and the df.name is the image set.
321+
pd.DataFrame
322+
A pandas DataFrame object containing the stimulus images for the
323+
experiment. Indices are image names, 'warped' and 'unwarped'
324+
columns contains image arrays, and the df.name is the image set.
332325
"""
333-
return self._stimulus_templates
326+
return self._stimulus_templates.to_dataframe()
334327

335328
@stimulus_templates.setter
336329
def stimulus_templates(self, value):

allensdk/brain_observatory/behavior/eye_tracking_processing.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def load_eye_tracking_hdf(eye_tracking_file: Path) -> pd.DataFrame:
4444

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

4949
return eye_tracking_data.astype(float)
5050

@@ -186,9 +186,23 @@ def process_eye_tracking_data(eye_data: pd.DataFrame,
186186
eye tracking frames.
187187
"""
188188

189-
if len(frame_times) != len(eye_data.index):
189+
n_sync = len(frame_times)
190+
n_eye_frames = len(eye_data.index)
191+
192+
# If n_sync exceeds n_eye_frames by <= 15,
193+
# just trim the excess sync pulses from the end
194+
# of the timestamps array.
195+
#
196+
# This solution was discussed in
197+
# https://github.com/AllenInstitute/AllenSDK/issues/1545
198+
199+
if n_sync > n_eye_frames and n_sync <= n_eye_frames+15:
200+
frame_times = frame_times[:n_eye_frames]
201+
n_sync = len(frame_times)
202+
203+
if n_sync != n_eye_frames:
190204
raise RuntimeError(f"Error! The number of sync file frame times "
191-
f"({len(frame_times)} does not match the "
205+
f"({len(frame_times)}) does not match the "
192206
f"number of eye tracking frames "
193207
f"({len(eye_data.index)})!")
194208

@@ -217,7 +231,7 @@ def process_eye_tracking_data(eye_data: pd.DataFrame,
217231
cr_areas[likely_blinks] = np.nan
218232
eye_areas[likely_blinks] = np.nan
219233

220-
eye_data.insert(0, "time", frame_times)
234+
eye_data.insert(0, "timestamps", frame_times)
221235
eye_data.insert(1, "cr_area", cr_areas)
222236
eye_data.insert(2, "eye_area", eye_areas)
223237
eye_data.insert(3, "pupil_area", pupil_areas)

allensdk/brain_observatory/behavior/metadata_processing.py

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
from typing import Dict
12
import re
3+
import numpy as np
24

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

5759

58-
def get_task_parameters(data):
60+
def get_task_parameters(data: Dict) -> Dict:
61+
"""
62+
Read task_parameters metadata from the behavior stimulus pickle file.
63+
64+
Parameters
65+
----------
66+
data: dict
67+
The nested dict read in from the behavior stimulus pickle file.
68+
All of the data expected by this method lives under
69+
data['items']['behavior']
70+
71+
Returns
72+
-------
73+
dict
74+
A dict containing the task_parameters associated with this session.
75+
"""
5976
behavior = data["items"]["behavior"]
77+
stimuli = behavior['stimuli']
78+
config = behavior["config"]
79+
doc = config["DoC"]
6080

6181
task_parameters = {}
82+
6283
task_parameters['blank_duration_sec'] = \
63-
[float(x) for x in behavior['config']['DoC']['blank_duration_range']]
64-
task_parameters['stimulus_duration_sec'] = \
65-
behavior['config']['DoC']['stimulus_window']
84+
[float(x) for x in doc['blank_duration_range']]
85+
86+
if 'images' in stimuli:
87+
stim_key = 'images'
88+
elif 'grating' in stimuli:
89+
stim_key = 'grating'
90+
else:
91+
msg = "Cannot get stimulus_duration_sec\n"
92+
msg += "'images' and/or 'grating' not a valid "
93+
msg += "key in pickle file under "
94+
msg += "['items']['behavior']['stimuli']\n"
95+
msg += f"keys: {list(stimuli.keys())}"
96+
raise RuntimeError(msg)
97+
98+
stim_duration = stimuli[stim_key]['flash_interval_sec']
99+
100+
# from discussion in
101+
# https://github.com/AllenInstitute/AllenSDK/issues/1572
102+
#
103+
# 'flash_interval' contains (stimulus_duration, gray_screen_duration)
104+
# (as @matchings said above). That second value is redundant with
105+
# 'blank_duration_range'. I'm not sure what would happen if they were
106+
# set to be conflicting values in the params. But it looks like
107+
# they're always consistent. It should always be (0.25, 0.5),
108+
# except for TRAINING_0 and TRAINING_1, which have statically
109+
# displayed stimuli (no flashes).
110+
111+
if stim_duration is None:
112+
stim_duration = np.NaN
113+
else:
114+
stim_duration = stim_duration[0]
115+
116+
task_parameters['stimulus_duration_sec'] = stim_duration
117+
66118
task_parameters['omitted_flash_fraction'] = \
67119
behavior['params'].get('flash_omit_probability', float('nan'))
68120
task_parameters['response_window_sec'] = \
69-
[float(x) for x in behavior["config"]["DoC"]["response_window"]]
70-
task_parameters['reward_volume'] = \
71-
behavior["config"]["reward"]["reward_volume"]
72-
task_parameters['stage'] = behavior["params"]["stage"]
121+
[float(x) for x in doc["response_window"]]
122+
task_parameters['reward_volume'] = config["reward"]["reward_volume"]
123+
task_parameters['auto_reward_volume'] = doc['auto_reward_volume']
124+
task_parameters['session_type'] = behavior["params"]["stage"]
73125
task_parameters['stimulus'] = next(iter(behavior["stimuli"]))
74-
task_parameters['stimulus_distribution'] = \
75-
behavior["config"]["DoC"]["change_time_dist"]
76-
task_parameters['task'] = behavior["config"]["behavior"]["task_id"]
126+
task_parameters['stimulus_distribution'] = doc["change_time_dist"]
127+
128+
task_id = config['behavior']['task_id']
129+
if 'DoC' in task_id:
130+
task_parameters['task'] = 'change detection'
131+
else:
132+
msg = "metadata_processing.get_task_parameters does not "
133+
msg += f"know how to parse 'task_id' = {task_id}"
134+
raise RuntimeError(msg)
135+
77136
n_stimulus_frames = 0
78137
for stim_type, stim_table in behavior["stimuli"].items():
79138
n_stimulus_frames += sum(stim_table.get("draw_log", []))

allensdk/brain_observatory/behavior/rewards_processing.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from typing import Dict
22
import numpy as np
33
import pandas as pd
4-
from collections import defaultdict
54

65

76
def get_rewards(data: Dict,
@@ -31,13 +30,14 @@ def get_rewards(data: Dict,
3130
trial_df = pd.DataFrame(data["items"]["behavior"]["trial_log"])
3231
rewards_dict = {"volume": [], "timestamps": [], "autorewarded": []}
3332
for idx, trial in trial_df.iterrows():
34-
rewards = trial["rewards"] # as i write this there can only ever be one reward per trial
33+
rewards = trial["rewards"]
34+
# as i write this there can only ever be one reward per trial
3535
if rewards:
3636
rewards_dict["volume"].append(rewards[0][0])
3737
rewards_dict["timestamps"].append(timestamps[rewards[0][2]])
3838
auto_rwrd = trial["trial_params"]["auto_reward"]
3939
rewards_dict["autorewarded"].append(auto_rwrd)
4040

41-
df = pd.DataFrame(rewards_dict).set_index("timestamps", drop=True)
41+
df = pd.DataFrame(rewards_dict)
4242

4343
return df

allensdk/brain_observatory/behavior/schemas.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ class BehaviorTaskParametersSchema(RaisingSchema):
215215
stimulus_duration_sec = fields.Float(
216216
doc='Duration of each stimulus presentation in seconds',
217217
required=True,
218+
allow_nan=True
218219
)
219220
omitted_flash_fraction = fields.Float(
220221
doc='Fraction of flashes/image presentations that were omitted',
@@ -232,7 +233,11 @@ class BehaviorTaskParametersSchema(RaisingSchema):
232233
doc='Volume of water (in mL) delivered as reward',
233234
required=True,
234235
)
235-
stage = fields.String(
236+
auto_reward_volume = fields.Float(
237+
doc='Volume of water (in mL) delivered as an automatic reward',
238+
required=True,
239+
)
240+
session_type = fields.String(
236241
doc='Stage of behavioral task',
237242
required=True,
238243
)

allensdk/brain_observatory/behavior/session_apis/data_io/behavior_nwb_api.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from allensdk.brain_observatory.behavior.schemas import (
1919
BehaviorTaskParametersSchema, OphysBehaviorMetadataSchema)
2020
from allensdk.brain_observatory.behavior.stimulus_processing import \
21-
StimulusTemplate
21+
StimulusTemplate, StimulusTemplateFactory
2222
from allensdk.brain_observatory.behavior.trials_processing import (
2323
TRIAL_COLUMN_DESCRIPTION_DICT
2424
)
@@ -75,12 +75,14 @@ def save(self, session_object):
7575
from_dataframe=True)
7676

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

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

201203
image_attributes = [{'image_name': image_name}
202204
for image_name in image_data.control_description]
203-
stimulus_templates = StimulusTemplate(
204-
image_set_name=image_set_name,
205-
image_attributes=image_attributes,
206-
images=image_data.data[:]
205+
return StimulusTemplateFactory.from_processed(
206+
image_set_name=image_set_name, image_attributes=image_attributes,
207+
warped=image_data.data[:], unwarped=image_data.unwarped[:]
207208
)
208-
return stimulus_templates
209209

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

226226
return pd.DataFrame({
227-
'time': licks.timestamps[:],
227+
'timestamps': licks.timestamps[:],
228228
'frame': licks.data[:]
229229
})
230230
else:
@@ -238,11 +238,11 @@ def get_rewards(self) -> np.ndarray:
238238
volume = rewards.get_data_interface('volume').data[:]
239239
return pd.DataFrame({
240240
'volume': volume, 'timestamps': time,
241-
'autorewarded': autorewarded}).set_index('timestamps')
241+
'autorewarded': autorewarded})
242242
else:
243243
return pd.DataFrame({
244244
'volume': [], 'timestamps': [],
245-
'autorewarded': []}).set_index('timestamps')
245+
'autorewarded': []})
246246

247247
def get_metadata(self) -> dict:
248248

0 commit comments

Comments
 (0)