diff --git a/CHANGELOG.md b/CHANGELOG.md index dfd57f722..153600238 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ ### Enhancements and minor changes - Added support for NWB schema 2.8.0: - - Fixed support for `data__bounds` field to `SpatialSeries` to set optional boundary range (min, max) for each dimension of data. Removed `SpatialSeries.bounds` field that was not functional. @rly [#1907](https://github.com/NeurodataWithoutBorders/pynwb/pull/1907) + - Removed `SpatialSeries.bounds` field that was not functional. This will be fixed in a future release. @rly [#1907](https://github.com/NeurodataWithoutBorders/pynwb/pull/1907), [#1996](https://github.com/NeurodataWithoutBorders/pynwb/pull/1996) + - Added support for `NWBFile.was_generated_by` field. @stephprince [#1924](https://github.com/NeurodataWithoutBorders/pynwb/pull/1924) + - Added support for `model_number`, `model_name`, and `serial_number` fields to `Device`. @stephprince [#1997](https://github.com/NeurodataWithoutBorders/pynwb/pull/1997) ## PyNWB 2.8.3 (Upcoming) diff --git a/docs/gallery/domain/ecephys.py b/docs/gallery/domain/ecephys.py index 509a491a1..3e49857a9 100644 --- a/docs/gallery/domain/ecephys.py +++ b/docs/gallery/domain/ecephys.py @@ -77,10 +77,16 @@ # # The electrodes table references a required :py:class:`~pynwb.ecephys.ElectrodeGroup`, which is used to represent a # group of electrodes. Before creating an :py:class:`~pynwb.ecephys.ElectrodeGroup`, you must define a -# :py:class:`~pynwb.device.Device` object using the method :py:meth:`.NWBFile.create_device`. - +# :py:class:`~pynwb.device.Device` object using the method :py:meth:`.NWBFile.create_device`. The fields +# ``description``, ``manufacturer``, ``model_number``, ``model_name``, and ``serial_number`` are optional, but +# recommended. device = nwbfile.create_device( - name="array", description="the best array", manufacturer="Probe Company 9000" + name="array", + description="A 12-channel array with 4 shanks and 3 channels per shank", + manufacturer="Array Technologies", + model_number="PRB_1_4_0480_123", + model_name="Neurovoxels 0.99", + serial_number="1234567890", ) ####################### diff --git a/docs/gallery/domain/ophys.py b/docs/gallery/domain/ophys.py index f8f6da98a..3f42e6c14 100644 --- a/docs/gallery/domain/ophys.py +++ b/docs/gallery/domain/ophys.py @@ -86,12 +86,17 @@ # :align: center # # Create a :py:class:`~pynwb.device.Device` named ``"Microscope"`` in the :py:class:`~pynwb.file.NWBFile` object. Then -# create an :py:class:`~pynwb.ophys.OpticalChannel` named ``"OpticalChannel"``. +# create an :py:class:`~pynwb.ophys.OpticalChannel` named ``"OpticalChannel"``. The fields +# ``description``, ``manufacturer``, ``model_number``, ``model_name``, and ``serial_number`` are optional, but +# recommended. device = nwbfile.create_device( name="Microscope", description="My two-photon microscope", - manufacturer="The best microscope manufacturer", + manufacturer="Loki Labs", + model_number="ABC-123", + model_name="Loki 1.0", + serial_number="1234567890", ) optical_channel = OpticalChannel( name="OpticalChannel", diff --git a/docs/gallery/domain/plot_behavior.py b/docs/gallery/domain/plot_behavior.py index 8d5b38d2a..8f341bea1 100644 --- a/docs/gallery/domain/plot_behavior.py +++ b/docs/gallery/domain/plot_behavior.py @@ -105,9 +105,6 @@ # # For position data ``reference_frame`` indicates the zero-position, e.g. # the 0,0 point might be the bottom-left corner of an enclosure, as viewed from the tracking camera. -# In :py:class:`~pynwb.behavior.SpatialSeries`, the ``data__bounds`` field allows the user to set -# the boundary range, i.e., (min, max), for each dimension of ``data``. The units are the same as in ``data``. -# This field does not enforce a boundary on the dataset itself. timestamps = np.linspace(0, 50) / 200 @@ -115,7 +112,6 @@ name="SpatialSeries", description="Position (x, y) in an open field.", data=position_data, - data__bounds=[(0,50), (0,50)], timestamps=timestamps, reference_frame="(0,0) is bottom left corner", ) diff --git a/src/pynwb/behavior.py b/src/pynwb/behavior.py index 7a647bc53..d4d43d515 100644 --- a/src/pynwb/behavior.py +++ b/src/pynwb/behavior.py @@ -20,14 +20,12 @@ class SpatialSeries(TimeSeries): tracking camera. The unit of data will indicate how to interpret SpatialSeries values. """ - __nwbfields__ = ('data__bounds', 'reference_frame',) + __nwbfields__ = ('reference_frame',) @docval(*get_docval(TimeSeries.__init__, 'name'), # required {'name': 'data', 'type': ('array_data', 'data', TimeSeries), 'shape': ((None, ), (None, None)), # required 'doc': ('The data values. Can be 1D or 2D. The first dimension must be time. If 2D, there can be 1, 2, ' 'or 3 columns, which represent x, y, and z.')}, - {'name': 'data__bounds', 'type': ('data', 'array_data'), 'shape': ((1, 2), (2, 2), (3, 2)), 'default': None, - 'doc': 'The boundary range (min, max) for each dimension of data.'}, {'name': 'reference_frame', 'type': str, 'doc': 'description defining what the zero-position is', 'default': None}, {'name': 'unit', 'type': str, 'doc': 'The base unit of measurement (should be SI unit)', @@ -38,8 +36,8 @@ def __init__(self, **kwargs): """ Create a SpatialSeries TimeSeries dataset """ - name, data, data__bounds, reference_frame, unit = popargs( - 'name', 'data', 'data__bounds', 'reference_frame', 'unit', kwargs + name, data, reference_frame, unit = popargs( + 'name', 'data', 'reference_frame', 'unit', kwargs ) super().__init__(name, data, unit, **kwargs) @@ -51,7 +49,6 @@ def __init__(self, **kwargs): "The second dimension should have length <= 3 to represent at most x, y, z." % (name, str(data_shape))) - self.data__bounds = data__bounds self.reference_frame = reference_frame @staticmethod diff --git a/src/pynwb/device.py b/src/pynwb/device.py index 6fcd610c8..f842776ae 100644 --- a/src/pynwb/device.py +++ b/src/pynwb/device.py @@ -10,18 +10,41 @@ class Device(NWBContainer): Metadata about a data acquisition device, e.g., recording system, electrode, microscope. """ - __nwbfields__ = ('name', - 'description', - 'manufacturer') + __nwbfields__ = ( + 'name', + 'description', + 'manufacturer', + 'model_number', + 'model_name', + 'serial_number', + ) - @docval({'name': 'name', 'type': str, 'doc': 'the name of this device'}, - {'name': 'description', 'type': str, - 'doc': 'Description of the device (e.g., model, firmware version, processing software version, etc.)', - 'default': None}, - {'name': 'manufacturer', 'type': str, 'doc': 'the name of the manufacturer of this device', - 'default': None}) + @docval( + {'name': 'name', 'type': str, 'doc': 'the name of this device'}, + {'name': 'description', 'type': str, + 'doc': ("Description of the device as free-form text. If there is any software/firmware associated " + "with the device, the names and versions of those can be added to `NWBFile.was_generated_by`."), + 'default': None}, + {'name': 'manufacturer', 'type': str, + 'doc': ("The name of the manufacturer of the device, e.g., Imec, Plexon, Thorlabs."), + 'default': None}, + {'name': 'model_number', 'type': str, + 'doc': ('The model number (or part/product number) of the device, e.g., PRB_1_4_0480_1, ' + 'PLX-VP-32-15SE(75)-(260-80)(460-10)-300-(1)CON/32m-V, BERGAMO.'), + 'default': None}, + {'name': 'model_name', 'type': str, + 'doc': ('The model name of the device, e.g., Neuropixels 1.0, V-Probe, Bergamo III.'), + 'default': None}, + {'name': 'serial_number', 'type': str, + 'doc': 'The serial number of the device.', + 'default': None}, + ) def __init__(self, **kwargs): - description, manufacturer = popargs('description', 'manufacturer', kwargs) + description, manufacturer, model_number, model_name, serial_number = popargs( + 'description', 'manufacturer', 'model_number', 'model_name', 'serial_number', kwargs) super().__init__(**kwargs) self.description = description self.manufacturer = manufacturer + self.model_number = model_number + self.model_name = model_name + self.serial_number = serial_number diff --git a/src/pynwb/file.py b/src/pynwb/file.py index 7621df2ac..a447c126d 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -261,6 +261,7 @@ class NWBFile(MultiContainerInterface, HERDManager): 'slices', 'source_script', 'source_script_file_name', + 'was_generated_by', 'data_collection', 'surgery', 'virus', @@ -339,6 +340,8 @@ class NWBFile(MultiContainerInterface, HERDManager): 'doc': 'Script file used to create this NWB file.', 'default': None}, {'name': 'source_script_file_name', 'type': str, 'doc': 'Name of the source_script file', 'default': None}, + {'name': 'was_generated_by', 'type': 'array_data', + 'doc': 'List of software package names and versions used to generate this NWB File.', 'default': None}, {'name': 'data_collection', 'type': str, 'doc': 'Notes about data collection and analysis.', 'default': None}, {'name': 'surgery', 'type': str, @@ -448,6 +451,7 @@ def __init__(self, **kwargs): 'slices', 'source_script', 'source_script_file_name', + 'was_generated_by', 'surgery', 'virus', 'stimulus_notes', diff --git a/src/pynwb/io/file.py b/src/pynwb/io/file.py index 90e8f36b7..53d257a05 100644 --- a/src/pynwb/io/file.py +++ b/src/pynwb/io/file.py @@ -97,6 +97,7 @@ def __init__(self, spec): 'session_id', 'slices', 'source_script', + 'was_generated_by', 'stimulus', 'surgery', 'virus'] diff --git a/src/pynwb/nwb-schema b/src/pynwb/nwb-schema index 041e48534..473fcc41e 160000 --- a/src/pynwb/nwb-schema +++ b/src/pynwb/nwb-schema @@ -1 +1 @@ -Subproject commit 041e48534da072caee475bb044da1a6c0991e0ee +Subproject commit 473fcc41e871288767cfb37d83315cca7469b9d1 diff --git a/tests/integration/hdf5/test_behavior.py b/tests/integration/hdf5/test_behavior.py index 60c6c4324..39770876b 100644 --- a/tests/integration/hdf5/test_behavior.py +++ b/tests/integration/hdf5/test_behavior.py @@ -11,7 +11,6 @@ def setUpContainer(self): return SpatialSeries( name='test_sS', data=np.ones((3, 2)), - data__bounds=[(-1,1),(-1,1),(-1,1)], reference_frame='reference_frame', timestamps=[1., 2., 3.] ) diff --git a/tests/integration/hdf5/test_device.py b/tests/integration/hdf5/test_device.py index d59af8f8b..820edbd53 100644 --- a/tests/integration/hdf5/test_device.py +++ b/tests/integration/hdf5/test_device.py @@ -6,9 +6,14 @@ class TestDeviceIO(NWBH5IOMixin, TestCase): def setUpContainer(self): """ Return the test Device to read/write """ - return Device(name='device_name', - description='description', - manufacturer='manufacturer') + return Device( + name='device_name', + description='description', + manufacturer='manufacturer', + model_number='model_number', + model_name='model_name', + serial_number='serial_number', + ) def addContainer(self, nwbfile): """ Add the test Device to the given NWBFile """ diff --git a/tests/integration/hdf5/test_nwbfile.py b/tests/integration/hdf5/test_nwbfile.py index e164ec649..24a08eb1b 100644 --- a/tests/integration/hdf5/test_nwbfile.py +++ b/tests/integration/hdf5/test_nwbfile.py @@ -42,6 +42,7 @@ def setUp(self): session_id='007', slices='noslices', source_script='nosources', + was_generated_by=[('nosoftware', '0.0.0')], surgery='nosurgery', virus='novirus', source_script_file_name='nofilename') @@ -128,6 +129,7 @@ def build_nwbfile(self): virus='a virus', source_script='noscript', source_script_file_name='nofilename', + was_generated_by=[('nosoftware', '0.0.0')], stimulus_notes='test stimulus notes', data_collection='test data collection notes', keywords=('these', 'are', 'keywords')) @@ -176,6 +178,32 @@ def build_nwbfile(self): self.nwbfile.experimenter = ('experimenter1', 'experimenter2') +class TestWasGeneratedByConstructorRoundtrip(TestNWBFileIO): + """ Test that a list of software packages / versions in a constructor is written to and read from file """ + + def build_nwbfile(self): + description = 'test nwbfile was_generated_by' + identifier = 'TEST_was_generated_by' + self.nwbfile = NWBFile(session_description=description, + identifier=identifier, + session_start_time=self.start_time, + was_generated_by=[('software1', '0.1.0'), + ('software2', '0.2.0'), + ('software3', '0.3.0')],) + +class TestWasGeneratedBySetterRoundtrip(TestNWBFileIO): + """ Test that a single tuple of software versions packages in a setter is written to and read from file """ + + def build_nwbfile(self): + description = 'test nwbfile was_generated_by' + identifier = 'TEST_was_generated_by' + self.nwbfile = NWBFile(session_description=description, + identifier=identifier, + session_start_time=self.start_time) + self.nwbfile.was_generated_by = [('software1', '0.1.0'), + ('software2', '0.2.0'), + ('software3', '0.3.0')] + class TestPublicationsConstructorRoundtrip(TestNWBFileIO): """ Test that a list of multiple publications in a constructor is written to and read from file """ diff --git a/tests/unit/test_behavior.py b/tests/unit/test_behavior.py index 8d5e2d4e5..37a471688 100644 --- a/tests/unit/test_behavior.py +++ b/tests/unit/test_behavior.py @@ -12,13 +12,11 @@ def test_init(self): sS = SpatialSeries( name='test_sS', data=np.ones((3, 2)), - data__bounds=[(-1,1),(-1,1),(-1,1)], reference_frame='reference_frame', timestamps=[1., 2., 3.] ) self.assertEqual(sS.name, 'test_sS') self.assertEqual(sS.unit, 'meters') - self.assertEqual(sS.data__bounds, [(-1,1),(-1,1),(-1,1)]) self.assertEqual(sS.reference_frame, 'reference_frame') def test_init_minimum(self): @@ -27,7 +25,6 @@ def test_init_minimum(self): data=np.ones((3, 2)), timestamps=[1., 2., 3.] ) - assert sS.bounds is None assert sS.reference_frame is None def test_set_unit(self): diff --git a/tests/unit/test_device.py b/tests/unit/test_device.py index 6e11346dd..305577b53 100644 --- a/tests/unit/test_device.py +++ b/tests/unit/test_device.py @@ -5,10 +5,18 @@ class TestDevice(TestCase): def test_init(self): - device = Device(name='device_name', - description='description', - manufacturer='manufacturer') + device = Device( + name='device_name', + description='description', + manufacturer='manufacturer', + model_number='model_number', + model_name='model_name', + serial_number='serial_number', + ) self.assertEqual(device.name, 'device_name') self.assertEqual(device.description, 'description') self.assertEqual(device.manufacturer, 'manufacturer') + self.assertEqual(device.model_number, 'model_number') + self.assertEqual(device.model_name, 'model_name') + self.assertEqual(device.serial_number, 'serial_number') diff --git a/tests/unit/test_file.py b/tests/unit/test_file.py index 8cb3415d9..23f993b02 100644 --- a/tests/unit/test_file.py +++ b/tests/unit/test_file.py @@ -42,6 +42,7 @@ def setUp(self): virus='a virus', source_script='noscript', source_script_file_name='nofilename', + was_generated_by=[('nosoftware', '0.0.0')], stimulus_notes='test stimulus notes', data_collection='test data collection notes', keywords=('these', 'are', 'keywords')) @@ -61,6 +62,7 @@ def test_constructor(self): self.assertEqual(self.nwbfile.related_publications, ('my pubs',)) self.assertEqual(self.nwbfile.source_script, 'noscript') self.assertEqual(self.nwbfile.source_script_file_name, 'nofilename') + self.assertEqual(self.nwbfile.was_generated_by, [('nosoftware', '0.0.0')]) self.assertEqual(self.nwbfile.keywords, ('these', 'are', 'keywords')) self.assertEqual(self.nwbfile.timestamps_reference_time, self.ref_time)