diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da2ccd12..8a7515c3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/gallery/domain/images.py b/docs/gallery/domain/images.py index 135e18c8c..d6eef24b3 100644 --- a/docs/gallery/domain/images.py +++ b/docs/gallery/domain/images.py @@ -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 diff --git a/docs/gallery/domain/plot_icephys.py b/docs/gallery/domain/plot_icephys.py index 410375909..8bb456b7f 100644 --- a/docs/gallery/domain/plot_icephys.py +++ b/docs/gallery/domain/plot_icephys.py @@ -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 @@ -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 # --------------------------------- diff --git a/docs/gallery/general/plot_file.py b/docs/gallery/general/plot_file.py index 154991877..5c59abf8d 100644 --- a/docs/gallery/general/plot_file.py +++ b/docs/gallery/general/plot_file.py @@ -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. @@ -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. diff --git a/docs/gallery/general/plot_read_basics.py b/docs/gallery/general/plot_read_basics.py index c4a829d75..0d684e5f5 100644 --- a/docs/gallery/general/plot_read_basics.py +++ b/docs/gallery/general/plot_read_basics.py @@ -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() diff --git a/docs/source/api_docs.rst b/docs/source/api_docs.rst index 3920ad316..94bf1957d 100644 --- a/docs/source/api_docs.rst +++ b/docs/source/api_docs.rst @@ -13,7 +13,6 @@ API Documentation Intracellular Electrophysiology Optophysiology Optogenetics - Retinotopy General Imaging Behavior NWB Base Classes diff --git a/docs/source/conf.py b/docs/source/conf.py index 6ceee9d50..6f69fc4c1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -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 @@ -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 diff --git a/src/pynwb/__init__.py b/src/pynwb/__init__.py index 607fe6a9e..9ea18efb8 100644 --- a/src/pynwb/__init__.py +++ b/src/pynwb/__init__.py @@ -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 diff --git a/src/pynwb/base.py b/src/pynwb/base.py index e9093db52..980d20d9a 100644 --- a/src/pynwb/base.py +++ b/src/pynwb/base.py @@ -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): diff --git a/src/pynwb/file.py b/src/pynwb/file.py index a8e9dd1b6..0b294e873 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -177,7 +177,7 @@ class NWBFile(MultiContainerInterface, HERDManager): { 'attr': 'stimulus', 'add': '_add_stimulus_internal', - 'type': TimeSeries, + 'type': (NWBDataInterface, DynamicTable), 'get': 'get_stimulus' }, { @@ -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, @@ -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'}) diff --git a/src/pynwb/icephys.py b/src/pynwb/icephys.py index bed2d4ecd..3101649fd 100644 --- a/src/pynwb/icephys.py +++ b/src/pynwb/icephys.py @@ -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')) @@ -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, @@ -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: @@ -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 @@ -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)) diff --git a/src/pynwb/io/__init__.py b/src/pynwb/io/__init__.py index b7e9bea48..e0de46b87 100644 --- a/src/pynwb/io/__init__.py +++ b/src/pynwb/io/__init__.py @@ -9,4 +9,3 @@ from . import misc as __misc from . import ogen as __ogen from . import ophys as __ophys -from . import retinotopy as __retinotopy diff --git a/src/pynwb/io/file.py b/src/pynwb/io/file.py index ccbfb8e47..1908c6b31 100644 --- a/src/pynwb/io/file.py +++ b/src/pynwb/io/file.py @@ -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')) diff --git a/src/pynwb/legacy/io/__init__.py b/src/pynwb/legacy/io/__init__.py index 97dc7b9d2..70e1d7a77 100644 --- a/src/pynwb/legacy/io/__init__.py +++ b/src/pynwb/legacy/io/__init__.py @@ -8,4 +8,3 @@ from . import misc as __misc from . import ogen as __ogen from . import ophys as __ophys -from . import retinotopy as __retinotopy diff --git a/src/pynwb/nwb-schema b/src/pynwb/nwb-schema index b4f8838cb..d65d42257 160000 --- a/src/pynwb/nwb-schema +++ b/src/pynwb/nwb-schema @@ -1 +1 @@ -Subproject commit b4f8838cbfbb7f8a117bd7e0aad19133d26868b4 +Subproject commit d65d42257003543c569ea7ac0cd6d7aee01c88d6 diff --git a/src/pynwb/ogen.py b/src/pynwb/ogen.py index f410b4235..af11842e4 100644 --- a/src/pynwb/ogen.py +++ b/src/pynwb/ogen.py @@ -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', diff --git a/src/pynwb/retinotopy.py b/src/pynwb/retinotopy.py index fd32037e8..b610ca37f 100644 --- a/src/pynwb/retinotopy.py +++ b/src/pynwb/retinotopy.py @@ -1,145 +1,151 @@ -import warnings -from collections.abc import Iterable - -from hdmf.utils import docval, popargs, get_docval - -from . import register_class, CORE_NAMESPACE -from .core import NWBDataInterface, NWBData - - -class RetinotopyImage(NWBData): - """Gray-scale anatomical image of cortical surface. Array structure: [rows][columns] - """ - - __nwbfields__ = ('bits_per_pixel', - 'dimension', - 'format', - 'field_of_view') - - @docval({'name': 'name', 'type': str, 'doc': 'Name of this retinotopy image'}, - {'name': 'data', 'type': Iterable, 'doc': 'Data field.'}, - {'name': 'bits_per_pixel', 'type': int, - 'doc': 'Number of bits used to represent each value. This is necessary to determine maximum ' - '(white) pixel value.'}, - {'name': 'dimension', 'type': Iterable, 'shape': (2, ), 'doc': 'Number of rows and columns in the image.'}, - {'name': 'format', 'type': Iterable, 'doc': 'Format of image. Right now only "raw" supported.'}, - {'name': 'field_of_view', 'type': Iterable, 'shape': (2, ), 'doc': 'Size of viewing area, in meters.'}) - def __init__(self, **kwargs): - bits_per_pixel, dimension, format, field_of_view = popargs( - 'bits_per_pixel', 'dimension', 'format', 'field_of_view', kwargs) - super().__init__(**kwargs) - self.bits_per_pixel = bits_per_pixel - self.dimension = dimension - self.format = format - self.field_of_view = field_of_view - - -class FocalDepthImage(RetinotopyImage): - """Gray-scale image taken with same settings/parameters (e.g., focal depth, - wavelength) as data collection. Array format: [rows][columns]. - """ - - __nwbfields__ = ('focal_depth', ) - - @docval(*get_docval(RetinotopyImage.__init__), - {'name': 'focal_depth', 'type': float, 'doc': 'Focal depth offset, in meters.'}) - def __init__(self, **kwargs): - focal_depth = popargs('focal_depth', kwargs) - super().__init__(**kwargs) - self.focal_depth = focal_depth - - -class RetinotopyMap(NWBData): - """Abstract two-dimensional map of responses to stimuli along a single response axis (e.g., altitude) - """ - - __nwbfields__ = ('field_of_view', - 'dimension') - - @docval({'name': 'name', 'type': str, 'doc': 'the name of this axis map'}, - {'name': 'data', 'type': Iterable, 'shape': (None, None), 'doc': 'data field.'}, - {'name': 'field_of_view', 'type': Iterable, 'shape': (2, ), 'doc': 'Size of viewing area, in meters.'}, - {'name': 'dimension', 'type': Iterable, 'shape': (2, ), - 'doc': 'Number of rows and columns in the image'}) - def __init__(self, **kwargs): - field_of_view, dimension = popargs('field_of_view', 'dimension', kwargs) - super().__init__(**kwargs) - self.field_of_view = field_of_view - self.dimension = dimension - - -class AxisMap(RetinotopyMap): - """Abstract two-dimensional map of responses to stimuli along a single response axis (e.g., altitude) with unit - """ - - __nwbfields__ = ('unit', ) - - @docval(*get_docval(RetinotopyMap.__init__, 'name', 'data', 'field_of_view'), - {'name': 'unit', 'type': str, 'doc': 'Unit that axis data is stored in (e.g., degrees)'}, - *get_docval(RetinotopyMap.__init__, 'dimension')) - def __init__(self, **kwargs): - unit = popargs('unit', kwargs) - super().__init__(**kwargs) - self.unit = unit - - -@register_class('ImagingRetinotopy', CORE_NAMESPACE) -class ImagingRetinotopy(NWBDataInterface): - """ - Intrinsic signal optical imaging or widefield imaging for measuring retinotopy. Stores orthogonal - maps (e.g., altitude/azimuth; radius/theta) of responses to specific stimuli and a combined - polarity map from which to identify visual areas. - This group does not store the raw responses imaged during retinotopic mapping or the - stimuli presented, but rather the resulting phase and power maps after applying a Fourier - transform on the averaged responses. - Note: for data consistency, all images and arrays are stored in the format [row][column] and - [row, col], which equates to [y][x]. Field of view and dimension arrays may appear backward - (i.e., y before x). - """ - - __nwbfields__ = ({'name': 'sign_map', 'child': True}, - {'name': 'axis_1_phase_map', 'child': True}, - {'name': 'axis_1_power_map', 'child': True}, - {'name': 'axis_2_phase_map', 'child': True}, - {'name': 'axis_2_power_map', 'child': True}, - {'name': 'focal_depth_image', 'child': True}, - {'name': 'vasculature_image', 'child': True}, - 'axis_descriptions') - - @docval({'name': 'sign_map', 'type': RetinotopyMap, - 'doc': 'Sine of the angle between the direction of the gradient in axis_1 and axis_2.'}, - {'name': 'axis_1_phase_map', 'type': AxisMap, - 'doc': 'Phase response to stimulus on the first measured axis.'}, - {'name': 'axis_1_power_map', 'type': AxisMap, - 'doc': 'Power response on the first measured axis. Response is scaled so 0.0 is no power in ' - 'the response and 1.0 is maximum relative power.'}, - {'name': 'axis_2_phase_map', 'type': AxisMap, - 'doc': 'Phase response to stimulus on the second measured axis.'}, - {'name': 'axis_2_power_map', 'type': AxisMap, - 'doc': 'Power response on the second measured axis. Response is scaled so 0.0 is no ' - 'power in the response and 1.0 is maximum relative power.'}, - {'name': 'axis_descriptions', 'type': Iterable, 'shape': (2, ), - 'doc': 'Two-element array describing the contents of the two response axis fields. ' - 'Description should be something like ["altitude", "azimuth"] or ["radius", "theta"].'}, - {'name': 'focal_depth_image', 'type': FocalDepthImage, - 'doc': 'Gray-scale image taken with same settings/parameters (e.g., focal depth, wavelength) ' - 'as data collection. Array format - [rows][columns].'}, - {'name': 'vasculature_image', 'type': RetinotopyImage, - 'doc': 'Gray-scale anatomical image of cortical surface. Array structure - [rows][columns].'}, - {'name': 'name', 'type': str, 'doc': 'the name of this container', 'default': 'ImagingRetinotopy'}) - def __init__(self, **kwargs): - axis_1_phase_map, axis_1_power_map, axis_2_phase_map, axis_2_power_map, axis_descriptions, \ - focal_depth_image, sign_map, vasculature_image = popargs( - 'axis_1_phase_map', 'axis_1_power_map', 'axis_2_phase_map', 'axis_2_power_map', - 'axis_descriptions', 'focal_depth_image', 'sign_map', 'vasculature_image', kwargs) - super().__init__(**kwargs) - warnings.warn("The ImagingRetinotopy class currently cannot be written to or read from a file. " - "This is a known bug and will be fixed in a future release of PyNWB.") - self.axis_1_phase_map = axis_1_phase_map - self.axis_1_power_map = axis_1_power_map - self.axis_2_phase_map = axis_2_phase_map - self.axis_2_power_map = axis_2_power_map - self.axis_descriptions = axis_descriptions - self.focal_depth_image = focal_depth_image - self.sign_map = sign_map - self.vasculature_image = vasculature_image +raise ImportError( + "The pynwb.retinotopy module is deprecated. If you are interested in using these neurodata types, " + "please create an issue on https://github.com/NeurodataWithoutBorders/nwb-schema/issues." +) + +# import warnings +# from collections.abc import Iterable + +# from hdmf.utils import docval, popargs, get_docval + +# from . import register_class, CORE_NAMESPACE +# from .core import NWBDataInterface, NWBData + + +# class RetinotopyImage(NWBData): +# """Gray-scale anatomical image of cortical surface. Array structure: [rows][columns] +# """ + +# __nwbfields__ = ('bits_per_pixel', +# 'dimension', +# 'format', +# 'field_of_view') + +# @docval({'name': 'name', 'type': str, 'doc': 'Name of this retinotopy image'}, +# {'name': 'data', 'type': Iterable, 'doc': 'Data field.'}, +# {'name': 'bits_per_pixel', 'type': int, +# 'doc': 'Number of bits used to represent each value. This is necessary to determine maximum ' +# '(white) pixel value.'}, +# {'name': 'dimension', 'type': Iterable, 'shape': (2, ), +# 'doc': 'Number of rows and columns in the image.'}, +# {'name': 'format', 'type': Iterable, 'doc': 'Format of image. Right now only "raw" supported.'}, +# {'name': 'field_of_view', 'type': Iterable, 'shape': (2, ), 'doc': 'Size of viewing area, in meters.'}) +# def __init__(self, **kwargs): +# bits_per_pixel, dimension, format, field_of_view = popargs( +# 'bits_per_pixel', 'dimension', 'format', 'field_of_view', kwargs) +# super().__init__(**kwargs) +# self.bits_per_pixel = bits_per_pixel +# self.dimension = dimension +# self.format = format +# self.field_of_view = field_of_view + + +# class FocalDepthImage(RetinotopyImage): +# """Gray-scale image taken with same settings/parameters (e.g., focal depth, +# wavelength) as data collection. Array format: [rows][columns]. +# """ + +# __nwbfields__ = ('focal_depth', ) + +# @docval(*get_docval(RetinotopyImage.__init__), +# {'name': 'focal_depth', 'type': float, 'doc': 'Focal depth offset, in meters.'}) +# def __init__(self, **kwargs): +# focal_depth = popargs('focal_depth', kwargs) +# super().__init__(**kwargs) +# self.focal_depth = focal_depth + + +# class RetinotopyMap(NWBData): +# """Abstract two-dimensional map of responses to stimuli along a single response axis (e.g., altitude) +# """ + +# __nwbfields__ = ('field_of_view', +# 'dimension') + +# @docval({'name': 'name', 'type': str, 'doc': 'the name of this axis map'}, +# {'name': 'data', 'type': Iterable, 'shape': (None, None), 'doc': 'data field.'}, +# {'name': 'field_of_view', 'type': Iterable, 'shape': (2, ), 'doc': 'Size of viewing area, in meters.'}, +# {'name': 'dimension', 'type': Iterable, 'shape': (2, ), +# 'doc': 'Number of rows and columns in the image'}) +# def __init__(self, **kwargs): +# field_of_view, dimension = popargs('field_of_view', 'dimension', kwargs) +# super().__init__(**kwargs) +# self.field_of_view = field_of_view +# self.dimension = dimension + + +# class AxisMap(RetinotopyMap): +# """Abstract two-dimensional map of responses to stimuli along a single response axis (e.g., altitude) with unit +# """ + +# __nwbfields__ = ('unit', ) + +# @docval(*get_docval(RetinotopyMap.__init__, 'name', 'data', 'field_of_view'), +# {'name': 'unit', 'type': str, 'doc': 'Unit that axis data is stored in (e.g., degrees)'}, +# *get_docval(RetinotopyMap.__init__, 'dimension')) +# def __init__(self, **kwargs): +# unit = popargs('unit', kwargs) +# super().__init__(**kwargs) +# self.unit = unit + + +# @register_class('ImagingRetinotopy', CORE_NAMESPACE) +# class ImagingRetinotopy(NWBDataInterface): +# """ +# Intrinsic signal optical imaging or widefield imaging for measuring retinotopy. Stores orthogonal +# maps (e.g., altitude/azimuth; radius/theta) of responses to specific stimuli and a combined +# polarity map from which to identify visual areas. +# This group does not store the raw responses imaged during retinotopic mapping or the +# stimuli presented, but rather the resulting phase and power maps after applying a Fourier +# transform on the averaged responses. +# Note: for data consistency, all images and arrays are stored in the format [row][column] and +# [row, col], which equates to [y][x]. Field of view and dimension arrays may appear backward +# (i.e., y before x). +# """ + +# __nwbfields__ = ({'name': 'sign_map', 'child': True}, +# {'name': 'axis_1_phase_map', 'child': True}, +# {'name': 'axis_1_power_map', 'child': True}, +# {'name': 'axis_2_phase_map', 'child': True}, +# {'name': 'axis_2_power_map', 'child': True}, +# {'name': 'focal_depth_image', 'child': True}, +# {'name': 'vasculature_image', 'child': True}, +# 'axis_descriptions') + +# @docval({'name': 'sign_map', 'type': RetinotopyMap, +# 'doc': 'Sine of the angle between the direction of the gradient in axis_1 and axis_2.'}, +# {'name': 'axis_1_phase_map', 'type': AxisMap, +# 'doc': 'Phase response to stimulus on the first measured axis.'}, +# {'name': 'axis_1_power_map', 'type': AxisMap, +# 'doc': 'Power response on the first measured axis. Response is scaled so 0.0 is no power in ' +# 'the response and 1.0 is maximum relative power.'}, +# {'name': 'axis_2_phase_map', 'type': AxisMap, +# 'doc': 'Phase response to stimulus on the second measured axis.'}, +# {'name': 'axis_2_power_map', 'type': AxisMap, +# 'doc': 'Power response on the second measured axis. Response is scaled so 0.0 is no ' +# 'power in the response and 1.0 is maximum relative power.'}, +# {'name': 'axis_descriptions', 'type': Iterable, 'shape': (2, ), +# 'doc': 'Two-element array describing the contents of the two response axis fields. ' +# 'Description should be something like ["altitude", "azimuth"] or ["radius", "theta"].'}, +# {'name': 'focal_depth_image', 'type': FocalDepthImage, +# 'doc': 'Gray-scale image taken with same settings/parameters (e.g., focal depth, wavelength) ' +# 'as data collection. Array format - [rows][columns].'}, +# {'name': 'vasculature_image', 'type': RetinotopyImage, +# 'doc': 'Gray-scale anatomical image of cortical surface. Array structure - [rows][columns].'}, +# {'name': 'name', 'type': str, 'doc': 'the name of this container', 'default': 'ImagingRetinotopy'}) +# def __init__(self, **kwargs): +# axis_1_phase_map, axis_1_power_map, axis_2_phase_map, axis_2_power_map, axis_descriptions, \ +# focal_depth_image, sign_map, vasculature_image = popargs( +# 'axis_1_phase_map', 'axis_1_power_map', 'axis_2_phase_map', 'axis_2_power_map', +# 'axis_descriptions', 'focal_depth_image', 'sign_map', 'vasculature_image', kwargs) +# super().__init__(**kwargs) +# warnings.warn("The ImagingRetinotopy class currently cannot be written to or read from a file. " +# "This is a known bug and will be fixed in a future release of PyNWB.") +# self.axis_1_phase_map = axis_1_phase_map +# self.axis_1_power_map = axis_1_power_map +# self.axis_2_phase_map = axis_2_phase_map +# self.axis_2_power_map = axis_2_power_map +# self.axis_descriptions = axis_descriptions +# self.focal_depth_image = focal_depth_image +# self.sign_map = sign_map +# self.vasculature_image = vasculature_image diff --git a/test.py b/test.py index 231facf58..dec0966d8 100644 --- a/test.py +++ b/test.py @@ -7,6 +7,7 @@ import logging import os.path import os +import shutil from subprocess import run, PIPE, STDOUT import sys import traceback @@ -279,6 +280,9 @@ def clean_up_tests(): "processed_data.nwb", "raw_data.nwb", "scratch_analysis.nwb", + "sub-P11HMH_ses-20061101_ecephys+image.nwb", + "test_edit.nwb", + "test_edit2.nwb", "test_cortical_surface.nwb", "test_icephys_file.nwb", "test_multicontainerinterface.extensions.yaml", @@ -290,6 +294,8 @@ def clean_up_tests(): if os.path.exists(name): os.remove(name) + shutil.rmtree("zarr_tutorial.nwb.zarr") + def main(): # setup and parse arguments diff --git a/tests/back_compat/test_import_structure.py b/tests/back_compat/test_import_structure.py index 79d4f6ad0..36831929d 100644 --- a/tests/back_compat/test_import_structure.py +++ b/tests/back_compat/test_import_structure.py @@ -78,7 +78,6 @@ def test_outer_import_structure(self): "popargs", "register_class", "register_map", - "retinotopy", "spec", "testing", "validate", diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py index f8c08f68f..5af4986ac 100644 --- a/tests/unit/test_base.py +++ b/tests/unit/test_base.py @@ -891,3 +891,9 @@ def test_data_property_bad_reference(self): IndexError, "'idx_start + count' out of range for timeseries 'test'" ): tsr.data + + def test_empty_reference_creation(self): + tsr = TimeSeriesReference.empty(self._create_time_series_with_rate()) + self.assertFalse(tsr.isvalid()) + self.assertIsNone(tsr.data) + self.assertIsNone(tsr.timestamps) diff --git a/tests/unit/test_file.py b/tests/unit/test_file.py index 17870acd4..98446fa46 100644 --- a/tests/unit/test_file.py +++ b/tests/unit/test_file.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from dateutil.tz import tzlocal, tzutc +from hdmf.common import DynamicTable from hdmf.common import VectorData from hdmf.utils import docval, get_docval, popargs @@ -151,6 +152,43 @@ def test_add_stimulus(self): 'grams', timestamps=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5])) self.assertEqual(len(self.nwbfile.stimulus), 1) + def test_add_stimulus_timeseries_arg(self): + """Test nwbfile.add_stimulus using the deprecated 'timeseries' keyword argument""" + msg = ( + "The 'timeseries' keyword argument is deprecated and will be removed in PyNWB 3.0. " + "Use the 'stimulus' argument instead." + ) + with self.assertWarnsWith(DeprecationWarning, msg): + self.nwbfile.add_stimulus( + timeseries=TimeSeries( + name='test_ts', + data=[0, 1, 2, 3, 4, 5], + unit='grams', + timestamps=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5] + ) + ) + self.assertEqual(len(self.nwbfile.stimulus), 1) + + def test_add_stimulus_no_stimulus_arg(self): + """Test nwbfile.add_stimulus using the deprecated 'timeseries' keyword argument""" + msg = ( + "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." + ) + with self.assertRaisesWith(ValueError, msg): + self.nwbfile.add_stimulus(None) + self.assertEqual(len(self.nwbfile.stimulus), 0) + + def test_add_stimulus_dynamic_table(self): + dt = DynamicTable( + name='test_dynamic_table', + description='a test dynamic table', + ) + self.nwbfile.add_stimulus(dt) + self.assertEqual(len(self.nwbfile.stimulus), 1) + self.assertIs(self.nwbfile.stimulus['test_dynamic_table'], dt) + def test_add_stimulus_template(self): self.nwbfile.add_stimulus_template(TimeSeries('test_ts', [0, 1, 2, 3, 4, 5], 'grams', timestamps=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5])) diff --git a/tests/unit/test_icephys_metadata_tables.py b/tests/unit/test_icephys_metadata_tables.py index 34531d6a4..b31ee9215 100644 --- a/tests/unit/test_icephys_metadata_tables.py +++ b/tests/unit/test_icephys_metadata_tables.py @@ -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() @@ -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() @@ -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): @@ -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( diff --git a/tests/unit/test_retinotopy.py b/tests/unit/test_retinotopy.py index 57942d274..9a9f67748 100644 --- a/tests/unit/test_retinotopy.py +++ b/tests/unit/test_retinotopy.py @@ -1,174 +1,182 @@ -import numpy as np - -from pynwb.retinotopy import ImagingRetinotopy, AxisMap, RetinotopyImage, FocalDepthImage, RetinotopyMap from pynwb.testing import TestCase -class ImageRetinotopyConstructor(TestCase): - - def setUp(self): - data = np.ones((2, 2)) - field_of_view = [1, 2] - dimension = [1, 2] - self.sign_map = RetinotopyMap('sign_map', data, field_of_view, dimension) - self.axis_1_phase_map = AxisMap('axis_1_phase_map', data, field_of_view, 'unit', dimension) - self.axis_1_power_map = AxisMap('axis_1_power_map', data, field_of_view, 'unit', dimension) - self.axis_2_phase_map = AxisMap('axis_2_phase_map', data, field_of_view, 'unit', dimension) - self.axis_2_power_map = AxisMap('axis_2_power_map', data, field_of_view, 'unit', dimension) - self.axis_descriptions = ['altitude', 'azimuth'] - - data = [[1, 1], [1, 1]] - bits_per_pixel = 8 - dimension = [3, 4] - format = 'raw' - field_of_view = [1, 2] - focal_depth = 1.0 - self.focal_depth_image = FocalDepthImage('focal_depth_image', data, bits_per_pixel, dimension, format, - field_of_view, focal_depth) - self.vasculature_image = RetinotopyImage('vasculature_image', np.uint16(data), bits_per_pixel, dimension, - format, field_of_view) - - def test_init(self): - """Test that ImagingRetinotopy constructor sets properties correctly.""" - msg = ('The ImagingRetinotopy class currently cannot be written to or read from a file. This is a known bug ' - 'and will be fixed in a future release of PyNWB.') - with self.assertWarnsWith(UserWarning, msg): - ir = ImagingRetinotopy(self.sign_map, self.axis_1_phase_map, self.axis_1_power_map, self.axis_2_phase_map, - self.axis_2_power_map, self.axis_descriptions, self.focal_depth_image, - self.vasculature_image) - self.assertEqual(ir.sign_map, self.sign_map) - self.assertEqual(ir.axis_1_phase_map, self.axis_1_phase_map) - self.assertEqual(ir.axis_1_power_map, self.axis_1_power_map) - self.assertEqual(ir.axis_2_phase_map, self.axis_2_phase_map) - self.assertEqual(ir.axis_2_power_map, self.axis_2_power_map) - self.assertEqual(ir.axis_descriptions, self.axis_descriptions) - self.assertEqual(ir.focal_depth_image, self.focal_depth_image) - self.assertEqual(ir.vasculature_image, self.vasculature_image) - - def test_init_axis_descriptions_wrong_shape(self): - """Test that creating a ImagingRetinotopy with a axis descriptions argument that is not 2 elements raises an - error. - """ - self.axis_descriptions = ['altitude', 'azimuth', 'extra'] - - msg = "ImagingRetinotopy.__init__: incorrect shape for 'axis_descriptions' (got '(3,)', expected '(2,)')" - with self.assertRaisesWith(ValueError, msg): - ImagingRetinotopy(self.sign_map, self.axis_1_phase_map, self.axis_1_power_map, self.axis_2_phase_map, - self.axis_2_power_map, self.axis_descriptions, self.focal_depth_image, - self.vasculature_image) - - -class RetinotopyImageConstructor(TestCase): - - def test_init(self): - """Test that RetinotopyImage constructor sets properties correctly.""" - data = [[1, 1], [1, 1]] - bits_per_pixel = 8 - dimension = [3, 4] - format = 'raw' - field_of_view = [1, 2] - image = RetinotopyImage('vasculature_image', data, bits_per_pixel, dimension, format, field_of_view) - - self.assertEqual(image.name, 'vasculature_image') - self.assertEqual(image.data, data) - self.assertEqual(image.bits_per_pixel, bits_per_pixel) - self.assertEqual(image.dimension, dimension) - self.assertEqual(image.format, format) - self.assertEqual(image.field_of_view, field_of_view) - - def test_init_dimension_wrong_shape(self): - """Test that creating a RetinotopyImage with a dimension argument that is not 2 elements raises an error.""" - data = [[1, 1], [1, 1]] - bits_per_pixel = 8 - dimension = [3, 4, 5] - format = 'raw' - field_of_view = [1, 2] - - msg = "RetinotopyImage.__init__: incorrect shape for 'dimension' (got '(3,)', expected '(2,)')" - with self.assertRaisesWith(ValueError, msg): - RetinotopyImage('vasculature_image', data, bits_per_pixel, dimension, format, field_of_view) - - def test_init_fov_wrong_shape(self): - """Test that creating a RetinotopyImage with a field of view argument that is not 2 elements raises an error.""" - data = [[1, 1], [1, 1]] - bits_per_pixel = 8 - dimension = [3, 4] - format = 'raw' - field_of_view = [1, 2, 3] - - msg = "RetinotopyImage.__init__: incorrect shape for 'field_of_view' (got '(3,)', expected '(2,)')" - with self.assertRaisesWith(ValueError, msg): - RetinotopyImage('vasculature_image', data, bits_per_pixel, dimension, format, field_of_view) - - -class RetinotopyMapConstructor(TestCase): - - def test_init(self): - """Test that RetinotopyMap constructor sets properties correctly.""" - data = np.ones((2, 2)) - field_of_view = [1, 2] - dimension = [1, 2] - map = RetinotopyMap('sign_map', data, field_of_view, dimension) - - self.assertEqual(map.name, 'sign_map') - np.testing.assert_array_equal(map.data, data) - self.assertEqual(map.field_of_view, field_of_view) - self.assertEqual(map.dimension, dimension) - - -class AxisMapConstructor(TestCase): - - def test_init(self): - """Test that AxisMap constructor sets properties correctly.""" - data = np.ones((2, 2)) - field_of_view = [1, 2] - dimension = [1, 2] - map = AxisMap('axis_1_phase', data, field_of_view, 'unit', dimension) - - self.assertEqual(map.name, 'axis_1_phase') - np.testing.assert_array_equal(map.data, data) - self.assertEqual(map.field_of_view, field_of_view) - self.assertEqual(map.dimension, dimension) - self.assertEqual(map.unit, 'unit') - - def test_init_dimension_wrong_shape(self): - """Test that creating an AxisMap with a dimension argument that is not 2 elements raises an error.""" - data = np.ones((2, 2)) - field_of_view = [1, 2] - dimension = [1, 2, 3] - - msg = "AxisMap.__init__: incorrect shape for 'dimension' (got '(3,)', expected '(2,)')" - with self.assertRaisesWith(ValueError, msg): - AxisMap('axis_1_phase', data, field_of_view, 'unit', dimension) - - def test_init_fov_wrong_shape(self): - """Test that creating an AxisMap with a dimension argument that is not 2 elements raises an error.""" - data = np.ones((2, 2)) - field_of_view = [1, 2, 3] - dimension = [1, 2] - - msg = "AxisMap.__init__: incorrect shape for 'field_of_view' (got '(3,)', expected '(2,)')" - with self.assertRaisesWith(ValueError, msg): - AxisMap('axis_1_phase', data, field_of_view, 'unit', dimension) - - -class FocalDepthImageConstructor(TestCase): - - def test_init(self): - """Test that FocalDepthImage constructor sets properties correctly.""" - data = [[1, 1], [1, 1]] - bits_per_pixel = 8 - dimension = [3, 4] - format = 'raw' - field_of_view = [1, 2] - focal_depth = 1.0 - image = FocalDepthImage('focal_depth_image', data, bits_per_pixel, dimension, format, field_of_view, - focal_depth) - - self.assertEqual(image.name, 'focal_depth_image') - self.assertEqual(image.data, data) - self.assertEqual(image.bits_per_pixel, bits_per_pixel) - self.assertEqual(image.dimension, dimension) - self.assertEqual(image.format, format) - self.assertEqual(image.field_of_view, field_of_view) - self.assertEqual(image.focal_depth, focal_depth) +class TestRetinotopy(TestCase): + def test_retinotopy_deprecated(self): + with self.assertRaises(ImportError): + import pynwb.retinotopy # noqa: F401 + +# import numpy as np + +# from pynwb.retinotopy import ImagingRetinotopy, AxisMap, RetinotopyImage, FocalDepthImage, RetinotopyMap +# from pynwb.testing import TestCase + + +# class ImageRetinotopyConstructor(TestCase): + +# def setUp(self): +# data = np.ones((2, 2)) +# field_of_view = [1, 2] +# dimension = [1, 2] +# self.sign_map = RetinotopyMap('sign_map', data, field_of_view, dimension) +# self.axis_1_phase_map = AxisMap('axis_1_phase_map', data, field_of_view, 'unit', dimension) +# self.axis_1_power_map = AxisMap('axis_1_power_map', data, field_of_view, 'unit', dimension) +# self.axis_2_phase_map = AxisMap('axis_2_phase_map', data, field_of_view, 'unit', dimension) +# self.axis_2_power_map = AxisMap('axis_2_power_map', data, field_of_view, 'unit', dimension) +# self.axis_descriptions = ['altitude', 'azimuth'] + +# data = [[1, 1], [1, 1]] +# bits_per_pixel = 8 +# dimension = [3, 4] +# format = 'raw' +# field_of_view = [1, 2] +# focal_depth = 1.0 +# self.focal_depth_image = FocalDepthImage('focal_depth_image', data, bits_per_pixel, dimension, format, +# field_of_view, focal_depth) +# self.vasculature_image = RetinotopyImage('vasculature_image', np.uint16(data), bits_per_pixel, dimension, +# format, field_of_view) + +# def test_init(self): +# """Test that ImagingRetinotopy constructor sets properties correctly.""" +# msg = ('The ImagingRetinotopy class currently cannot be written to or read from a file. This is a known bug ' +# 'and will be fixed in a future release of PyNWB.') +# with self.assertWarnsWith(UserWarning, msg): +# ir = ImagingRetinotopy(self.sign_map, self.axis_1_phase_map, self.axis_1_power_map, self.axis_2_phase_map, +# self.axis_2_power_map, self.axis_descriptions, self.focal_depth_image, +# self.vasculature_image) +# self.assertEqual(ir.sign_map, self.sign_map) +# self.assertEqual(ir.axis_1_phase_map, self.axis_1_phase_map) +# self.assertEqual(ir.axis_1_power_map, self.axis_1_power_map) +# self.assertEqual(ir.axis_2_phase_map, self.axis_2_phase_map) +# self.assertEqual(ir.axis_2_power_map, self.axis_2_power_map) +# self.assertEqual(ir.axis_descriptions, self.axis_descriptions) +# self.assertEqual(ir.focal_depth_image, self.focal_depth_image) +# self.assertEqual(ir.vasculature_image, self.vasculature_image) + +# def test_init_axis_descriptions_wrong_shape(self): +# """Test that creating a ImagingRetinotopy with a axis descriptions argument that is not 2 elements raises an +# error. +# """ +# self.axis_descriptions = ['altitude', 'azimuth', 'extra'] + +# msg = "ImagingRetinotopy.__init__: incorrect shape for 'axis_descriptions' (got '(3,)', expected '(2,)')" +# with self.assertRaisesWith(ValueError, msg): +# ImagingRetinotopy(self.sign_map, self.axis_1_phase_map, self.axis_1_power_map, self.axis_2_phase_map, +# self.axis_2_power_map, self.axis_descriptions, self.focal_depth_image, +# self.vasculature_image) + + +# class RetinotopyImageConstructor(TestCase): + +# def test_init(self): +# """Test that RetinotopyImage constructor sets properties correctly.""" +# data = [[1, 1], [1, 1]] +# bits_per_pixel = 8 +# dimension = [3, 4] +# format = 'raw' +# field_of_view = [1, 2] +# image = RetinotopyImage('vasculature_image', data, bits_per_pixel, dimension, format, field_of_view) + +# self.assertEqual(image.name, 'vasculature_image') +# self.assertEqual(image.data, data) +# self.assertEqual(image.bits_per_pixel, bits_per_pixel) +# self.assertEqual(image.dimension, dimension) +# self.assertEqual(image.format, format) +# self.assertEqual(image.field_of_view, field_of_view) + +# def test_init_dimension_wrong_shape(self): +# """Test that creating a RetinotopyImage with a dimension argument that is not 2 elements raises an error.""" +# data = [[1, 1], [1, 1]] +# bits_per_pixel = 8 +# dimension = [3, 4, 5] +# format = 'raw' +# field_of_view = [1, 2] + +# msg = "RetinotopyImage.__init__: incorrect shape for 'dimension' (got '(3,)', expected '(2,)')" +# with self.assertRaisesWith(ValueError, msg): +# RetinotopyImage('vasculature_image', data, bits_per_pixel, dimension, format, field_of_view) + +# def test_init_fov_wrong_shape(self): +# """Test that creating a RetinotopyImage with a field of view that is not 2 elements raises an error.""" +# data = [[1, 1], [1, 1]] +# bits_per_pixel = 8 +# dimension = [3, 4] +# format = 'raw' +# field_of_view = [1, 2, 3] + +# msg = "RetinotopyImage.__init__: incorrect shape for 'field_of_view' (got '(3,)', expected '(2,)')" +# with self.assertRaisesWith(ValueError, msg): +# RetinotopyImage('vasculature_image', data, bits_per_pixel, dimension, format, field_of_view) + + +# class RetinotopyMapConstructor(TestCase): + +# def test_init(self): +# """Test that RetinotopyMap constructor sets properties correctly.""" +# data = np.ones((2, 2)) +# field_of_view = [1, 2] +# dimension = [1, 2] +# map = RetinotopyMap('sign_map', data, field_of_view, dimension) + +# self.assertEqual(map.name, 'sign_map') +# np.testing.assert_array_equal(map.data, data) +# self.assertEqual(map.field_of_view, field_of_view) +# self.assertEqual(map.dimension, dimension) + + +# class AxisMapConstructor(TestCase): + +# def test_init(self): +# """Test that AxisMap constructor sets properties correctly.""" +# data = np.ones((2, 2)) +# field_of_view = [1, 2] +# dimension = [1, 2] +# map = AxisMap('axis_1_phase', data, field_of_view, 'unit', dimension) + +# self.assertEqual(map.name, 'axis_1_phase') +# np.testing.assert_array_equal(map.data, data) +# self.assertEqual(map.field_of_view, field_of_view) +# self.assertEqual(map.dimension, dimension) +# self.assertEqual(map.unit, 'unit') + +# def test_init_dimension_wrong_shape(self): +# """Test that creating an AxisMap with a dimension argument that is not 2 elements raises an error.""" +# data = np.ones((2, 2)) +# field_of_view = [1, 2] +# dimension = [1, 2, 3] + +# msg = "AxisMap.__init__: incorrect shape for 'dimension' (got '(3,)', expected '(2,)')" +# with self.assertRaisesWith(ValueError, msg): +# AxisMap('axis_1_phase', data, field_of_view, 'unit', dimension) + +# def test_init_fov_wrong_shape(self): +# """Test that creating an AxisMap with a dimension argument that is not 2 elements raises an error.""" +# data = np.ones((2, 2)) +# field_of_view = [1, 2, 3] +# dimension = [1, 2] + +# msg = "AxisMap.__init__: incorrect shape for 'field_of_view' (got '(3,)', expected '(2,)')" +# with self.assertRaisesWith(ValueError, msg): +# AxisMap('axis_1_phase', data, field_of_view, 'unit', dimension) + + +# class FocalDepthImageConstructor(TestCase): + +# def test_init(self): +# """Test that FocalDepthImage constructor sets properties correctly.""" +# data = [[1, 1], [1, 1]] +# bits_per_pixel = 8 +# dimension = [3, 4] +# format = 'raw' +# field_of_view = [1, 2] +# focal_depth = 1.0 +# image = FocalDepthImage('focal_depth_image', data, bits_per_pixel, dimension, format, field_of_view, +# focal_depth) + +# self.assertEqual(image.name, 'focal_depth_image') +# self.assertEqual(image.data, data) +# self.assertEqual(image.bits_per_pixel, bits_per_pixel) +# self.assertEqual(image.dimension, dimension) +# self.assertEqual(image.format, format) +# self.assertEqual(image.field_of_view, field_of_view) +# self.assertEqual(image.focal_depth, focal_depth)