Skip to content

Commit 7e60bc5

Browse files
Merge pull request #1618 from AllenInstitute/rc/2.1.0
2.1.0 release
2 parents 4168859 + 79138f4 commit 7e60bc5

18 files changed

+434
-49
lines changed

CHANGELOG.md

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

4+
## [2.1.0] = 2020-07-16
5+
6+
### Added
7+
- Behvaior Ophys NWB File writing capability fixes for updated PyNWB and HDMF versions
8+
- Added warning if using outdated Visual Coding Neuropixels NWB files
9+
- Added documentation file for Visual Behavior terms in AllenSDK for quick lookup
10+
411
## [2.0.0] = 2020-06-11
512

613
### Added

allensdk/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
import logging
3737

3838

39-
__version__ = '2.0.2'
39+
40+
__version__ = '2.1.0'
4041

4142

4243
try:

allensdk/brain_observatory/behavior/behavior_ophys_api/behavior_ophys_nwb_api.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from allensdk.core.lazy_property import LazyProperty
1818
from allensdk.brain_observatory.nwb.nwb_api import NwbApi
19+
from allensdk.brain_observatory.nwb.nwb_utils import set_omitted_stop_time
1920
from allensdk.brain_observatory.behavior.trials_processing import TRIAL_COLUMN_DESCRIPTION_DICT
2021
from allensdk.brain_observatory.behavior.schemas import OphysBehaviorMetaDataSchema, OphysBehaviorTaskParametersSchema
2122
from allensdk.brain_observatory.nwb.metadata import load_LabMetaData_extension
@@ -59,6 +60,9 @@ def save(self, session_object):
5960
stimulus_index = session_object.stimulus_presentations[session_object.stimulus_presentations['image_set'] == nwb_template.name]
6061
nwb.add_stimulus_index(nwbfile, stimulus_index, nwb_template)
6162

63+
# search for omitted rows and add stop_time before writing to NWB file
64+
set_omitted_stop_time(stimulus_table=session_object.stimulus_presentations)
65+
6266
# Add stimulus presentations data to NWB in-memory object:
6367
nwb.add_stimulus_presentations(nwbfile, session_object.stimulus_presentations)
6468

allensdk/brain_observatory/behavior/stimulus_processing.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,17 @@ def load_pickle(pstream):
3030
return pickle.load(pstream, encoding="bytes")
3131

3232

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

162-
def get_visual_stimuli_df(data, time):
171+
172+
def get_visual_stimuli_df(data, time) -> pd.DataFrame:
173+
"""
174+
This function loads the stimuli and the omitted stimuli into a dataframe.
175+
These stimuli are loaded from the input data, where the set_log and
176+
draw_log contained within are used to calculate the epochs. These epochs
177+
are used as start_frame and end_frame and converted to times by input
178+
stimulus timestamps. The omitted stimuli do not have a end_frame by design
179+
though there duration is always 250ms.
180+
:param data: the behavior data file
181+
:param time: the stimulus timestamps indicating when each stimuli is
182+
displayed
183+
:return: df: a pandas dataframe containing the stimuli and omitted stimuli
184+
that were displayed with their frame, end_frame, start_time,
185+
and duration
186+
"""
163187

164188
stimuli = data['items']['behavior']['stimuli']
165189
n_frames = len(time)

allensdk/brain_observatory/ecephys/ecephys_session_api/ecephys_nwb_session_api.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from typing import Dict, Union, List, Optional, Callable
22
import re
33
import ast
4+
import warnings
45

6+
import h5py
57
import pandas as pd
68
import numpy as np
79
import xarray as xr
@@ -11,6 +13,7 @@
1113
from allensdk.brain_observatory.nwb.nwb_api import NwbApi
1214
import allensdk.brain_observatory.ecephys.nwb # noqa Necessary to import pyNWB namespaces
1315
from allensdk.brain_observatory.ecephys import get_unit_filter_value
16+
from allensdk.brain_observatory.nwb import check_nwbfile_version
1417

1518
color_triplet_re = re.compile(r"\[(-{0,1}\d*\.\d*,\s*)*(-{0,1}\d*\.\d*)\]")
1619

@@ -55,6 +58,18 @@ def __init__(self,
5558
self.additional_unit_metrics = additional_unit_metrics
5659
self.external_channel_columns = external_channel_columns
5760

61+
if hasattr(self, "path") and self.path:
62+
check_nwbfile_version(
63+
nwbfile_path=self.path,
64+
desired_minimum_version="2.2.2",
65+
warning_msg=(
66+
f"It looks like the Visual Coding Neuropixels nwbfile "
67+
f"you are trying to access ({self.path})"
68+
f"was created by a previous (and incompatible) version of "
69+
f"AllenSDK and pynwb. You will need to either 1) use "
70+
f"AllenSDK version < 2.0.0 or 2) re-download an updated "
71+
f"version of the nwbfile to access the desired data."))
72+
5873
def test(self):
5974
""" A minimal test to make sure that this API's NWB file exists and is
6075
readable. Ecephys NWB files use the required session identifier field

allensdk/brain_observatory/nwb/__init__.py

Lines changed: 90 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import logging
2+
import warnings
23
from pathlib import Path
3-
from typing import Iterable
4+
from typing import Iterable, Tuple
45

6+
import h5py
57
import numpy as np
68
import pandas as pd
79
import datetime
@@ -10,11 +12,12 @@
1012
import pynwb
1113
from pynwb.base import TimeSeries, Images
1214
from pynwb.behavior import BehavioralEvents
13-
from pynwb import ProcessingModule
15+
from pynwb import ProcessingModule, NWBFile
1416
from pynwb.image import ImageSeries, GrayscaleImage, IndexSeries
1517
from pynwb.ophys import DfOverF, ImageSegmentation, OpticalChannel, Fluorescence
1618

1719
import allensdk.brain_observatory.roi_masks as roi
20+
from allensdk.brain_observatory.nwb.nwb_utils import (get_column_name)
1821
from allensdk.brain_observatory.running_speed import RunningSpeed
1922
from allensdk.brain_observatory import dict_to_indexed_array
2023
from allensdk.brain_observatory.behavior.image_api import Image
@@ -25,6 +28,50 @@
2528

2629
log = logging.getLogger("allensdk.brain_observatory.nwb")
2730

31+
CELL_SPECIMEN_COL_DESCRIPTIONS = {
32+
'cell_specimen_id': 'Unified id of segmented cell across experiments (after'
33+
' cell matching)',
34+
'height': 'Height of ROI in pixels',
35+
'width': 'Width of ROI in pixels',
36+
'mask_image_plane': 'Which image plane an ROI resides on. Overlapping ROIs '
37+
'are stored on different mask image planes.',
38+
'max_correction_down': 'Max motion correction in down direction in pixels',
39+
'max_correction_left': 'Max motion correction in left direction in pixels',
40+
'max_correction_up': 'Max motion correction in up direction in pixels',
41+
'max_correction_right': 'Max motion correction in right direction in '
42+
'pixels',
43+
'valid_roi': 'Indicates if cell classification found the ROI to be a cell '
44+
'or not',
45+
'x': 'x position of ROI in Image Plane in pixels (top left corner)',
46+
'y': 'y position of ROI in Image Plane in pixels (top left corner)'
47+
}
48+
49+
def check_nwbfile_version(nwbfile_path: str,
50+
desired_minimum_version: str,
51+
warning_msg: str):
52+
53+
with h5py.File(nwbfile_path, 'r') as f:
54+
# nwb 2.x files store version as an attribute
55+
try:
56+
nwb_version = str(f.attrs["nwb_version"]).split(".")
57+
except KeyError:
58+
# nwb 1.x files store version as dataset
59+
try:
60+
nwb_version = str(f["nwb_version"][...].astype(str))
61+
# Stored in the form: `NWB-x.y.z`
62+
nwb_version = nwb_version.split("-")[1].split(".")
63+
except (KeyError, IndexError):
64+
nwb_version = None
65+
66+
if nwb_version is None:
67+
warnings.warn(f"'{nwbfile_path}' doesn't appear to be a valid "
68+
f"Neurodata Without Borders (*.nwb) format file as "
69+
f"neither a 'nwb_version' field nor dataset could "
70+
f"be found!")
71+
else:
72+
if tuple(nwb_version) < tuple(desired_minimum_version.split(".")):
73+
warnings.warn(warning_msg)
74+
2875

2976
def read_eye_dlc_tracking_ellipses(input_path: Path) -> dict:
3077
"""Reads eye tracking ellipse fit data from an h5 file.
@@ -424,10 +471,13 @@ def add_stimulus_presentations(nwbfile, stimulus_table, tag='stimulus_time_inter
424471
"""
425472
stimulus_table = stimulus_table.copy()
426473
ts = nwbfile.modules['stimulus'].get_data_interface('timestamps')
427-
stimulus_names = stimulus_table['stimulus_name'].unique()
474+
possible_names = {'stimulus_name', 'image_name'}
475+
stimulus_name_column = get_column_name(stimulus_table.columns,
476+
possible_names)
477+
stimulus_names = stimulus_table[stimulus_name_column].unique()
428478

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

448498
for row in cleaned_table.itertuples(index=False):
449499
row = row._asdict()
500+
450501
presentation_interval.add_interval(**row, tags=tag, timeseries=ts)
451502

452503
nwbfile.add_time_intervals(presentation_interval)
@@ -709,7 +760,28 @@ def add_task_parameters(nwbfile, task_parameters):
709760
nwbfile.add_lab_meta_data(nwb_task_parameters)
710761

711762

712-
def add_cell_specimen_table(nwbfile, cell_specimen_table):
763+
def add_cell_specimen_table(nwbfile: NWBFile,
764+
cell_specimen_table: pd.DataFrame):
765+
"""
766+
This function takes the cell specimen table and writes the ROIs
767+
contained within. It writes these to a new NWB imaging plane
768+
based off the previously supplied metadata
769+
Parameters
770+
----------
771+
nwbfile: NWBFile
772+
this is the in memory NWBFile currently being written to which ROI data
773+
is added
774+
cell_specimen_table: pd.DataFrame
775+
this is the DataFrame containing the cells segmented from a ophys
776+
experiment, stored in json file and loaded.
777+
example: /home/nicholasc/projects/allensdk/allensdk/test/
778+
brain_observatory/behavior/cell_specimen_table_789359614.json
779+
780+
Returns
781+
-------
782+
nwbfile: NWBFile
783+
The altered in memory NWBFile object that now has a specimen table
784+
"""
713785
cell_roi_table = cell_specimen_table.reset_index().set_index('cell_roi_id')
714786

715787
# Device:
@@ -769,12 +841,21 @@ def add_cell_specimen_table(nwbfile, cell_specimen_table):
769841
description="Segmented rois",
770842
imaging_plane=imaging_plane)
771843

772-
for c in [c for c in cell_roi_table.columns if c not in ['id', 'mask_matrix']]:
773-
plane_segmentation.add_column(c, c)
774-
844+
for col_name in cell_roi_table.columns:
845+
# the columns 'image_mask', 'pixel_mask', and 'voxel_mask' are already defined
846+
# in the nwb.ophys::PlaneSegmentation Object
847+
if col_name not in ['id', 'mask_matrix', 'image_mask', 'pixel_mask', 'voxel_mask']:
848+
# This builds the columns with name of column and description of column
849+
# both equal to the column name in the cell_roi_table
850+
plane_segmentation.add_column(col_name,
851+
CELL_SPECIMEN_COL_DESCRIPTIONS.get(col_name,
852+
"No Description Available"))
853+
854+
# go through each roi and add it to the plan segmentation object
775855
for cell_roi_id, row in cell_roi_table.iterrows():
776856
sub_mask = np.array(row.pop('image_mask'))
777-
curr_roi = roi.create_roi_mask(fov_width, fov_height, [(fov_width - 1), 0, (fov_height - 1), 0], roi_mask=sub_mask)
857+
curr_roi = roi.create_roi_mask(fov_width, fov_height, [(fov_width - 1), 0, (fov_height - 1), 0],
858+
roi_mask=sub_mask)
778859
mask = curr_roi.get_mask_plane()
779860
csid = row.pop('cell_specimen_id')
780861
row['cell_specimen_id'] = -1 if csid is None else csid
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import pandas as pd
2+
# All of the omitted stimuli have a duration of 250ms as defined
3+
# by the Visual Behavior team. For questions about duration contact that
4+
# team.
5+
6+
7+
def get_column_name(table_cols: list,
8+
possible_names: set) -> str:
9+
"""
10+
This function returns a column name, given a table with unknown
11+
column names and a set of possible column names which are expected.
12+
The table column name returned should be the only name contained in
13+
the "expected" possible names.
14+
:param table_cols: the table columns to search for the possible name within
15+
:param possible_names: the names that could exist within the data columns
16+
:return: the first entry of the intersection between the possible names
17+
and the names of the columns of the stimulus table
18+
"""
19+
20+
column_set = set(table_cols)
21+
column_names = list(column_set.intersection(possible_names))
22+
if not len(column_names) == 1:
23+
raise KeyError("Table expected one name column in intersection, found:"
24+
f" {column_names}")
25+
return column_names[0]
26+
27+
28+
def set_omitted_stop_time(stimulus_table: pd.DataFrame,
29+
omitted_time_duration: float=0.25) -> None:
30+
"""
31+
This function sets the stop time for a row that of a stimuli table that
32+
is a omitted stimuli. A omitted stimuli is a stimuli where a mouse is
33+
shown only a grey screen and these last for 250 milliseconds. These do not
34+
include a stop_time or end_frame as other stimuli in the stimulus table due
35+
to design choices. For these stimuli to be added they must have the
36+
stop_time calculated and put into the row as data before writing to NWB.
37+
:param stimulus_table: pd.DataFrame that contains the stimuli presented to
38+
an experiment subject
39+
:param omitted_time_duration: The duration in seconds of the expected length
40+
of the omitted stimuli
41+
:return:
42+
stimulus_table_row: returns the same dictionary as inputted but with
43+
an additional entry for stop_time.
44+
"""
45+
omitted_row_indexs = stimulus_table.index[stimulus_table['omitted']].tolist()
46+
for omitted_row_idx in omitted_row_indexs:
47+
row = stimulus_table.iloc[omitted_row_idx]
48+
start_time = row['start_time']
49+
end_time = start_time + omitted_time_duration
50+
row['stop_time'] = end_time
51+
row['duration'] = omitted_time_duration
52+
stimulus_table.iloc[omitted_row_idx] = row

allensdk/test/brain_observatory/behavior/test_behavior_data_lims_api.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,28 @@
66
import math
77

88
from allensdk.internal.api.behavior_data_lims_api import BehaviorDataLimsApi
9+
from allensdk.core.authentication import DbCredentials
910
from allensdk.internal.api.behavior_ophys_api import BehaviorOphysLimsApi
1011
from allensdk.brain_observatory.running_speed import RunningSpeed
1112
from allensdk.core.exceptions import DataFrameIndexError
1213

14+
mock_db_credentials = DbCredentials(dbname='mock_db', user='mock_user',
15+
host='mock_host', port='mock_port',
16+
password='mock')
17+
1318

1419
@pytest.fixture
1520
def MockBehaviorDataLimsApi():
21+
1622
class MockBehaviorDataLimsApi(BehaviorDataLimsApi):
1723
"""
1824
Mock class that overrides some functions to provide test data and
1925
initialize without calls to db.
2026
"""
2127
def __init__(self):
22-
super().__init__(behavior_session_id=8675309)
28+
super().__init__(behavior_session_id=8675309,
29+
lims_credentials=mock_db_credentials,
30+
mtrain_credentials=mock_db_credentials)
2331

2432
def _get_ids(self):
2533
return {}
@@ -73,7 +81,9 @@ class MockApiRunSpeedExpectedError(BehaviorDataLimsApi):
7381
initialize without calls to db.
7482
"""
7583
def __init__(self):
76-
super().__init__(behavior_session_id=8675309)
84+
super().__init__(behavior_session_id=8675309,
85+
mtrain_credentials=mock_db_credentials,
86+
lims_credentials=mock_db_credentials)
7787

7888
def _get_ids(self):
7989
return {}

allensdk/test/brain_observatory/behavior/test_behavior_ophys_session.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ def test_session_from_json(tmpdir_factory, session_data, get_expected, get_from_
4141
compare_fields(expected, obtained)
4242

4343

44-
@pytest.mark.xfail
4544
@pytest.mark.requires_bamboo
4645
def test_nwb_end_to_end(tmpdir_factory):
4746
oeid = 789359614

0 commit comments

Comments
 (0)