Skip to content

Commit

Permalink
Merge pull request #1820 from NeurodataWithoutBorders/nwb-schema-2.7.0
Browse files Browse the repository at this point in the history
Add support for NWB schema 2.7.0
  • Loading branch information
rly authored Mar 26, 2024
2 parents 270291a + 02702a7 commit 8ebd8db
Show file tree
Hide file tree
Showing 23 changed files with 601 additions and 337 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
## PyNWB 2.7.0 (Upcoming)

### Enhancements and minor changes
- Added support for NWB schema 2.7.0. See [2.7.0 release notes](https://nwb-schema.readthedocs.io/en/latest/format_release_notes.html) for details
- Deprecated `ImagingRetinotopy` neurodata type. @rly [#1813](https://github.com/NeurodataWithoutBorders/pynwb/pull/1813)
- Modified `OptogeneticSeries` to allow 2D data, primarily in extensions of `OptogeneticSeries`. @rly [#1812](https://github.com/NeurodataWithoutBorders/pynwb/pull/1812)
- Support `stimulus_template` as optional predefined column in `IntracellularStimuliTable`. @stephprince [#1815](https://github.com/NeurodataWithoutBorders/pynwb/pull/1815)
- Support `NWBDataInterface` and `DynamicTable` in `NWBFile.stimulus`. @rly [#1842](https://github.com/NeurodataWithoutBorders/pynwb/pull/1842)
- Added support for python 3.12 and upgraded dependency versions. This also includes infrastructure updates for developers. @mavaylon1 [#1853](https://github.com/NeurodataWithoutBorders/pynwb/pull/1853)

### Bug fixes
Expand Down
2 changes: 1 addition & 1 deletion docs/gallery/domain/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
description="The images presented to the subject as stimuli",
)

nwbfile.add_stimulus(timeseries=optical_series)
nwbfile.add_stimulus(stimulus=optical_series)

####################
# ImageSeries: Storing series of images as acquisition
Expand Down
54 changes: 54 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 TimeSeriesReference, TimeSeriesReferenceVectorData

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

#####################################################################
# Adding stimulus templates
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#
# One predefined subcategory column is the ``stimulus_template`` column in the stimuli table. This column is
# used to store template waveforms of stimuli in addition to the actual recorded stimulus that is stored in the
# ``stimulus`` column. The ``stimulus_template`` column contains an idealized version of the template waveform used as
# the stimulus. This can be useful as a noiseless version of the stimulus for data analysis or to validate that the
# recorded stimulus matches the expected waveform of the template. Similar to the ``stimulus`` and ``response``
# columns, we can specify a relevant time range.

stimulus_template = VoltageClampStimulusSeries(
name="ccst",
data=[0, 1, 2, 3, 4],
starting_time=0.0,
rate=10e3,
electrode=electrode,
gain=0.02,
)
nwbfile.add_stimulus_template(stimulus_template)

nwbfile.intracellular_recordings.add_column(
name="stimulus_template",
data=[TimeSeriesReference(0, 5, stimulus_template), # (start_index, index_count, stimulus_template)
TimeSeriesReference(1, 3, stimulus_template),
TimeSeriesReference.empty(stimulus_template)], # if there was no data for that recording, use empty reference
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_template, # the full time range of the stimulus template will be used unless specified
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
# :py:meth:`~pynwb.file.NWBFile.add_intracellular_recording` will internally set the stimulus template 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.

#####################################################################
# .. note:: Since stimulus templates are often reused across many recordings, the timestamps in the templates are not
# usually aligned with the recording nor with the reference time of the file. The timestamps often start
# at 0 and are relative to the time of the application of the stimulus.

#####################################################################
# Add a simultaneous recording
# ---------------------------------
Expand Down
5 changes: 2 additions & 3 deletions docs/gallery/general/plot_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@
:py:class:`~pynwb.ophys.ImageSegmentation`,
:py:class:`~pynwb.ophys.MotionCorrection`.
* **Others:** :py:class:`~pynwb.retinotopy.ImagingRetinotopy`,
:py:class:`~pynwb.base.Images`.
* **Others:** :py:class:`~pynwb.base.Images`.
* **TimeSeries:** Any :ref:`timeseries_overview` is also a subclass of :py:class:`~pynwb.core.NWBDataInterface`
and can be used anywhere :py:class:`~pynwb.core.NWBDataInterface` is allowed.
Expand Down Expand Up @@ -372,7 +371,7 @@
# Processing modules can be thought of as folders within the file for storing the related processed data.
#
# .. tip:: Use the NWB schema module names as processing module names where appropriate.
# These are: ``"behavior"``, ``"ecephys"``, ``"icephys"``, ``"ophys"``, ``"ogen"``, ``"retinotopy"``, and ``"misc"``.
# These are: ``"behavior"``, ``"ecephys"``, ``"icephys"``, ``"ophys"``, ``"ogen"``, and ``"misc"``.
#
# Let's assume that the subject's position was computed from a video tracking algorithm,
# so it would be classified as processed data.
Expand Down
7 changes: 7 additions & 0 deletions docs/gallery/general/plot_read_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,10 @@
# object and accessing its attributes, but it may be useful to explore the data in a
# more interactive, visual way. See :ref:`analysistools-explore` for an updated list of programs for
# exploring NWB files.

####################
# Close the open NWB file
# -----------------------
# It is good practice, especially on Windows, to close any files that you have opened.

io.close()
1 change: 0 additions & 1 deletion docs/source/api_docs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ API Documentation
Intracellular Electrophysiology <pynwb.icephys>
Optophysiology <pynwb.ophys>
Optogenetics <pynwb.ogen>
Retinotopy <pynwb.retinotopy>
General Imaging <pynwb.image>
Behavior <pynwb.behavior>
NWB Base Classes <pynwb.base>
Expand Down
9 changes: 8 additions & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ def __call__(self, filename):
# directories to ignore when looking for source files.
exclude_patterns = ['_build', 'test.py']

# List of patterns, relative to source directory, of modules to be
# excluded by apidoc when generating rst files.
apidoc_exclude = [
"../../src/pynwb/retinotopy.py",
]

# The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None

Expand Down Expand Up @@ -410,7 +416,8 @@ def run_apidoc(_):
out_dir = os.path.dirname(__file__)
src_dir = os.path.join(out_dir, '../../src')
sys.path.append(src_dir)
apidoc_main(['-f', '-e', '--no-toc', '-o', out_dir, src_dir])
apidoc_exclude_abs = [os.path.join(out_dir, f) for f in apidoc_exclude]
apidoc_main(['-f', '-e', '--no-toc', '-o', out_dir, src_dir, *apidoc_exclude_abs])


from abc import abstractproperty
Expand Down
1 change: 0 additions & 1 deletion src/pynwb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,6 @@ def export(self, **kwargs):
from . import misc # noqa: F401,E402
from . import ogen # noqa: F401,E402
from . import ophys # noqa: F401,E402
from . import retinotopy # noqa: F401,E402
from . import legacy # noqa: F401,E402
from hdmf.data_utils import DataChunkIterator # noqa: F401,E402
from hdmf.backends.hdf5 import H5DataIO # noqa: F401,E402
Expand Down
20 changes: 20 additions & 0 deletions src/pynwb/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,26 @@ def data(self):
# load the data from the timeseries
return self.timeseries.data[self.idx_start: (self.idx_start + self.count)]

@classmethod
@docval({'name': 'timeseries', 'type': TimeSeries, 'doc': 'the timeseries object to reference.'})
def empty(cls, timeseries):
"""
Creates an empty TimeSeriesReference object to represent missing data.
When missing data needs to be represented, NWB defines ``None`` for the complex data type ``(idx_start,
count, TimeSeries)`` as (-1, -1, TimeSeries) for storage. The exact timeseries object will technically not
matter since the empty reference is a way of indicating a NaN value in a
:py:class:`~pynwb.base.TimeSeriesReferenceVectorData` column.
An example where this functionality is used is :py:class:`~pynwb.icephys.IntracellularRecordingsTable`
where only one of stimulus or response data was recorded. In such cases, the timeseries object for the
empty stimulus :py:class:`~pynwb.base.TimeSeriesReference` could be set to the response series, or vice versa.
:returns: Returns :py:class:`~pynwb.base.TimeSeriesReference`
"""

return cls(-1, -1, timeseries)


@register_class('TimeSeriesReferenceVectorData', CORE_NAMESPACE)
class TimeSeriesReferenceVectorData(VectorData):
Expand Down
30 changes: 23 additions & 7 deletions src/pynwb/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ class NWBFile(MultiContainerInterface, HERDManager):
{
'attr': 'stimulus',
'add': '_add_stimulus_internal',
'type': TimeSeries,
'type': (NWBDataInterface, DynamicTable),
'get': 'get_stimulus'
},
{
Expand Down Expand Up @@ -356,7 +356,8 @@ class NWBFile(MultiContainerInterface, HERDManager):
{'name': 'analysis', 'type': (list, tuple),
'doc': 'result of analysis', 'default': None},
{'name': 'stimulus', 'type': (list, tuple),
'doc': 'Stimulus TimeSeries objects belonging to this NWBFile', 'default': None},
'doc': 'Stimulus TimeSeries, DynamicTable, or NWBDataInterface objects belonging to this NWBFile',
'default': None},
{'name': 'stimulus_template', 'type': (list, tuple),
'doc': 'Stimulus template TimeSeries objects belonging to this NWBFile', 'default': None},
{'name': 'epochs', 'type': TimeIntervals,
Expand Down Expand Up @@ -856,14 +857,29 @@ def add_acquisition(self, **kwargs):
if use_sweep_table:
self._update_sweep_table(nwbdata)

@docval({'name': 'timeseries', 'type': TimeSeries},
{'name': 'use_sweep_table', 'type': bool, 'default': False, 'doc': 'Use the deprecated SweepTable'})
@docval({'name': 'stimulus', 'type': (TimeSeries, DynamicTable, NWBDataInterface), 'default': None,
'doc': 'The stimulus presentation data to add to this NWBFile.'},
{'name': 'use_sweep_table', 'type': bool, 'default': False, 'doc': 'Use the deprecated SweepTable'},
{'name': 'timeseries', 'type': TimeSeries, 'default': None,
'doc': 'The "timeseries" keyword argument is deprecated. Use the "nwbdata" argument instead.'},)
def add_stimulus(self, **kwargs):
timeseries = popargs('timeseries', kwargs)
self._add_stimulus_internal(timeseries)
stimulus, timeseries = popargs('stimulus', 'timeseries', kwargs)
if stimulus is None and timeseries is None:
raise ValueError(
"The 'stimulus' keyword argument is required. The 'timeseries' keyword argument can be "
"provided for backwards compatibility but is deprecated in favor of 'stimulus' and will be "
"removed in PyNWB 3.0."
)
# TODO remove this support in PyNWB 3.0
if timeseries is not None:
warn("The 'timeseries' keyword argument is deprecated and will be removed in PyNWB 3.0. "
"Use the 'stimulus' argument instead.", DeprecationWarning)
if stimulus is None:
stimulus = timeseries
self._add_stimulus_internal(stimulus)
use_sweep_table = popargs('use_sweep_table', kwargs)
if use_sweep_table:
self._update_sweep_table(timeseries)
self._update_sweep_table(stimulus)

@docval({'name': 'timeseries', 'type': (TimeSeries, Images)},
{'name': 'use_sweep_table', 'type': bool, 'default': False, 'doc': 'Use the deprecated SweepTable'})
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
1 change: 0 additions & 1 deletion src/pynwb/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@
from . import misc as __misc
from . import ogen as __ogen
from . import ophys as __ophys
from . import retinotopy as __retinotopy
6 changes: 5 additions & 1 deletion src/pynwb/io/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ def __init__(self, spec):
self.unmap(stimulus_spec)
self.unmap(stimulus_spec.get_group('presentation'))
self.unmap(stimulus_spec.get_group('templates'))
self.map_spec('stimulus', stimulus_spec.get_group('presentation').get_neurodata_type('TimeSeries'))
# map "stimulus" to NWBDataInterface and DynamicTable and unmap the spec for TimeSeries because it is
# included in the mapping to NWBDataInterface
self.unmap(stimulus_spec.get_group('presentation').get_neurodata_type('TimeSeries'))
self.map_spec('stimulus', stimulus_spec.get_group('presentation').get_neurodata_type('NWBDataInterface'))
self.map_spec('stimulus', stimulus_spec.get_group('presentation').get_neurodata_type('DynamicTable'))
self.map_spec('stimulus_template', stimulus_spec.get_group('templates').get_neurodata_type('TimeSeries'))
self.map_spec('stimulus_template', stimulus_spec.get_group('templates').get_neurodata_type('Images'))

Expand Down
1 change: 0 additions & 1 deletion src/pynwb/legacy/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@
from . import misc as __misc
from . import ogen as __ogen
from . import ophys as __ophys
from . import retinotopy as __retinotopy
5 changes: 3 additions & 2 deletions src/pynwb/ogen.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ class OptogeneticSeries(TimeSeries):
__nwbfields__ = ('site',)

@docval(*get_docval(TimeSeries.__init__, 'name'), # required
{'name': 'data', 'type': ('array_data', 'data', TimeSeries), 'shape': (None, ), # required
'doc': 'The data values over time. Must be 1D.'},
{'name': 'data', 'type': ('array_data', 'data', TimeSeries), # required
'shape': [(None, ), (None, None)],
'doc': 'The data values over time.'},
{'name': 'site', 'type': OptogeneticStimulusSite, # required
'doc': 'The site to which this stimulus was applied.'},
*get_docval(TimeSeries.__init__, 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate',
Expand Down
Loading

0 comments on commit 8ebd8db

Please sign in to comment.