Skip to content

Commit

Permalink
Merge branch 'nwb_schema_2.8.0' into deprecate_event_waveform
Browse files Browse the repository at this point in the history
  • Loading branch information
rly committed Nov 19, 2024
2 parents e0489f9 + 52a7e6d commit 80f03f4
Show file tree
Hide file tree
Showing 15 changed files with 110 additions and 37 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
12 changes: 9 additions & 3 deletions docs/gallery/domain/ecephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)

#######################
Expand Down
9 changes: 7 additions & 2 deletions docs/gallery/domain/ophys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 0 additions & 4 deletions docs/gallery/domain/plot_behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,13 @@
#
# 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

position_spatial_series = SpatialSeries(
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",
)
Expand Down
9 changes: 3 additions & 6 deletions src/pynwb/behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand All @@ -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)

Expand All @@ -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
Expand Down
43 changes: 33 additions & 10 deletions src/pynwb/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions src/pynwb/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ class NWBFile(MultiContainerInterface, HERDManager):
'slices',
'source_script',
'source_script_file_name',
'was_generated_by',
'data_collection',
'surgery',
'virus',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -448,6 +451,7 @@ def __init__(self, **kwargs):
'slices',
'source_script',
'source_script_file_name',
'was_generated_by',
'surgery',
'virus',
'stimulus_notes',
Expand Down
1 change: 1 addition & 0 deletions src/pynwb/io/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def __init__(self, spec):
'session_id',
'slices',
'source_script',
'was_generated_by',
'stimulus',
'surgery',
'virus']
Expand Down
1 change: 0 additions & 1 deletion tests/integration/hdf5/test_behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.]
)
Expand Down
11 changes: 8 additions & 3 deletions tests/integration/hdf5/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
28 changes: 28 additions & 0 deletions tests/integration/hdf5/test_nwbfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -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 """

Expand Down
3 changes: 0 additions & 3 deletions tests/unit/test_behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
14 changes: 11 additions & 3 deletions tests/unit/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
2 changes: 2 additions & 0 deletions tests/unit/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand All @@ -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)

Expand Down

0 comments on commit 80f03f4

Please sign in to comment.