Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support stimulus_template as optional column in IntracellularRecordingsTable #1815

Merged
merged 16 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
33 changes: 33 additions & 0 deletions docs/gallery/domain/plot_icephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@

# Import additional core datatypes used in the example
from pynwb.core import DynamicTable, VectorData
from pynwb.base import TimeSeriesReferenceVectorData

# Import icephys TimeSeries types used
from pynwb.icephys import VoltageClampSeries, VoltageClampStimulusSeries
Expand Down Expand Up @@ -457,6 +458,38 @@
category="electrodes",
)

#####################################################################
# One predefined subcategory column is the stimulus template column in the stimuli table. This column can be used to
oruebel marked this conversation as resolved.
Show resolved Hide resolved
# store stimulus template waveforms that were generated by the recording software. Similar to the stimulus and response
# columns, we can specify a relevant time range.
stephprince marked this conversation as resolved.
Show resolved Hide resolved

nwbfile.intracellular_recordings.add_column(
name="stimulus_template",
data=[(0, 5, stimulus), # (start_index, index_count, stimulus_template) can be specified as a tuple
(1, 3, stimulus),
(-1, -1, stimulus)],
oruebel marked this conversation as resolved.
Show resolved Hide resolved
description="Column storing the reference to the stimulus template for the recording (rows).",
category="stimuli",
col_cls=TimeSeriesReferenceVectorData
)

# we can also add stimulus template data as follows
rowindex = nwbfile.add_intracellular_recording(
electrode=electrode,
stimulus=stimulus,
stimulus_template=stimulus, # the full time range of the stimulus template will be used unless specified
oruebel marked this conversation as resolved.
Show resolved Hide resolved
recording_tag='A4',
recording_lab_data={'location': 'Isengard'},
electrode_metadata={'voltage_threshold': 0.14},
id=13,
)

#####################################################################
# .. note:: If a stimulus template column exists but there is no stimulus template data for that recording, then the
# stimulus template will be internally set to the provided stimulus or response TimeSeries and the start_index
# and index_count for the missing parameter are set to -1. The missing values will be represented via masked
# numpy arrays.

#####################################################################
# Add a simultaneous recording
# ---------------------------------
Expand Down
30 changes: 30 additions & 0 deletions src/pynwb/icephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,12 @@ class IntracellularStimuliTable(DynamicTable):
'index': False,
'table': False,
'class': TimeSeriesReferenceVectorData},
{'name': 'stimulus_template',
'description': 'Column storing the reference to the stimulus template for the recording (rows)',
'required': False,
'index': False,
'table': False,
'class': TimeSeriesReferenceVectorData},
)

@docval(*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'))
Expand Down Expand Up @@ -518,6 +524,13 @@ def __init__(self, **kwargs):
{'name': 'stimulus', 'type': TimeSeries,
'doc': 'The TimeSeries (usually a PatchClampSeries) with the stimulus',
'default': None},
{'name': 'stimulus_template_start_index', 'type': int, 'doc': 'Start index of the stimulus template',
'default': None},
{'name': 'stimulus_template_index_count', 'type': int, 'doc': 'Stop index of the stimulus template',
'default': None},
{'name': 'stimulus_template', 'type': TimeSeries,
'doc': 'The TimeSeries (usually a PatchClampSeries) with the stimulus template waveforms',
'default': None},
{'name': 'response_start_index', 'type': int, 'doc': 'Start index of the response', 'default': None},
{'name': 'response_index_count', 'type': int, 'doc': 'Stop index of the response', 'default': None},
{'name': 'response', 'type': TimeSeries,
Expand Down Expand Up @@ -553,6 +566,11 @@ def add_recording(self, **kwargs):
'response',
kwargs)
electrode = popargs('electrode', kwargs)
stimulus_template_start_index, stimulus_template_index_count, stimulus_template = popargs(
'stimulus_template_start_index',
'stimulus_template_index_count',
'stimulus_template',
kwargs)

# if electrode is not provided, take from stimulus or response object
if electrode is None:
Expand All @@ -572,6 +590,15 @@ def add_recording(self, **kwargs):
response_start_index, response_index_count = self.__compute_index(response_start_index,
response_index_count,
response, 'response')
stimulus_template_start_index, stimulus_template_index_count = self.__compute_index(
stimulus_template_start_index,
stimulus_template_index_count,
stimulus_template, 'stimulus_template')

# if stimulus template is already a column in the stimuli table, but stimulus_template was None
if 'stimulus_template' in self.category_tables['stimuli'].colnames and stimulus_template is None:
stimulus_template = stimulus if stimulus is not None else response # set to stimulus if it was provided

# If either stimulus or response are None, then set them to the same TimeSeries to keep the I/O happy
response = response if response is not None else stimulus
stimulus_provided_is_not_none = stimulus is not None # Store if stimulus is None for error checks later
Expand Down Expand Up @@ -612,6 +639,9 @@ def add_recording(self, **kwargs):
stimuli = {}
stimuli['stimulus'] = TimeSeriesReferenceVectorData.TIME_SERIES_REFERENCE_TUPLE(
stimulus_start_index, stimulus_index_count, stimulus)
if stimulus_template is not None:
stimuli['stimulus_template'] = TimeSeriesReferenceVectorData.TIME_SERIES_REFERENCE_TUPLE(
stimulus_template_start_index, stimulus_template_index_count, stimulus_template)

# Compile the responses table data
responses = copy(popargs('response_metadata', kwargs))
Expand Down
62 changes: 62 additions & 0 deletions tests/unit/test_icephys_metadata_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,15 @@ def test_add_row_index_out_of_range(self):
response=self.response,
id=np.int64(10)
)
with self.assertRaises(IndexError):
ir = IntracellularRecordingsTable()
ir.add_recording(
electrode=self.electrode,
stimulus_template=self.stimulus,
stimulus_template_start_index=10,
response=self.response,
id=np.int64(10)
)
# Stimulus/Response index count too large
with self.assertRaises(IndexError):
ir = IntracellularRecordingsTable()
Expand All @@ -438,6 +447,15 @@ def test_add_row_index_out_of_range(self):
response=self.response,
id=np.int64(10)
)
with self.assertRaises(IndexError):
ir = IntracellularRecordingsTable()
ir.add_recording(
electrode=self.electrode,
stimulus_template=self.stimulus,
stimulus_template_index_count=10,
response=self.response,
id=np.int64(10)
)
# Stimulus/Response start+count combination too large
with self.assertRaises(IndexError):
ir = IntracellularRecordingsTable()
Expand All @@ -459,6 +477,16 @@ def test_add_row_index_out_of_range(self):
response=self.response,
id=np.int64(10)
)
with self.assertRaises(IndexError):
ir = IntracellularRecordingsTable()
ir.add_recording(
electrode=self.electrode,
stimulus_template=self.stimulus,
stimulus_template_start_index=3,
stimulus_template_index_count=4,
response=self.response,
id=np.int64(10)
)

def test_add_row_no_stimulus_and_response(self):
with self.assertRaises(ValueError):
Expand All @@ -469,6 +497,40 @@ def test_add_row_no_stimulus_and_response(self):
response=None
)

def test_add_row_with_stimulus_template(self):
ir = IntracellularRecordingsTable()
ir.add_recording(
electrode=self.electrode,
stimulus=self.stimulus,
stimulus_template=self.stimulus,
response=self.response,
id=np.int64(10)
)

def test_add_stimulus_template_column(self):
ir = IntracellularRecordingsTable()
ir.add_column(name='stimulus_template',
description='test column',
category='stimuli',
col_cls=TimeSeriesReferenceVectorData)

def test_add_row_with_no_stimulus_template_when_stimulus_template_column_exists(self):
ir = IntracellularRecordingsTable()
ir.add_recording(electrode=self.electrode,
stimulus=self.stimulus,
response=self.response,
stimulus_template=self.stimulus,
id=np.int64(10))

# add row with only stimulus when stimulus template column already exists
ir.add_recording(electrode=self.electrode,
stimulus=self.stimulus,
id=np.int64(20))
# add row with only response when stimulus template column already exists
ir.add_recording(electrode=self.electrode,
response=self.stimulus,
id=np.int64(30))

def test_add_column(self):
ir = IntracellularRecordingsTable()
ir.add_recording(
Expand Down
Loading