diff --git a/CHANGELOG.md b/CHANGELOG.md index 21e9e8ae7..ebec466ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Enhancements and minor changes - Added `pynwb.read_nwb` convenience method to simplify reading an NWBFile written with any backend @h-mayorquin [#1994](https://github.com/NeurodataWithoutBorders/pynwb/pull/1994) + +### Bug fixes +- Made distance, orientation, and field_of_view optional in OpticalSeries to match schema @bendichter [#2023](https://github.com/NeurodataWithoutBorders/pynwb/pull/2023) - Added support for NWB schema 2.8.0. @rly [#2001](https://github.com/NeurodataWithoutBorders/pynwb/pull/2001) - 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) diff --git a/src/pynwb/image.py b/src/pynwb/image.py index c775297d7..4dc3ab6e0 100644 --- a/src/pynwb/image.py +++ b/src/pynwb/image.py @@ -325,12 +325,26 @@ class OpticalSeries(ImageSeries): 'orientation') @docval(*get_docval(ImageSeries.__init__, 'name'), # required - {'name': 'distance', 'type': float, 'doc': 'Distance from camera/monitor to target/eye.'}, # required - {'name': 'field_of_view', 'type': ('array_data', 'data', 'TimeSeries'), 'shape': ((2, ), (3, )), # required - 'doc': 'Width, height and depth of image, or imaged area (meters).'}, - {'name': 'orientation', 'type': str, # required - 'doc': 'Description of image relative to some reference frame (e.g., which way is up). ' - 'Must also specify frame of reference.'}, + { + "name": "distance", + "type": float, + "doc": "Distance from camera/monitor to target/eye.", + "default": None, + }, + { + "name": "field_of_view", + "type": ("array_data", "data", "TimeSeries"), + "shape": ((2,), (3,)), + "doc": "Width, height and depth of image, or imaged area (meters).", + "default": None, + }, + { + "name": "orientation", + "type": str, + "doc": "Description of image relative to some reference frame (e.g., which way is up). " + "Must also specify frame of reference.", + "default": None, + }, {'name': 'data', 'type': ('array_data', 'data'), 'shape': ([None] * 3, [None, None, None, 3]), 'doc': ('Images presented to subject, either grayscale or RGB. May be 3D or 4D. The first dimension must ' 'be time (frame). The second and third dimensions represent x and y. The optional fourth ' diff --git a/tests/unit/test_image.py b/tests/unit/test_image.py index 92637d82e..34f65ad9e 100644 --- a/tests/unit/test_image.py +++ b/tests/unit/test_image.py @@ -2,7 +2,7 @@ import numpy as np -from pynwb import TimeSeries +from pynwb import TimeSeries, NWBHDF5IO from pynwb.base import Image, Images, ImageReferences from pynwb.device import Device from pynwb.image import ( @@ -396,6 +396,54 @@ def test_init(self): self.assertEqual(ts.format, 'external') + def test_init_all_optional_fields_none(self): + """Test that OpticalSeries can be created with all optional fields set to None.""" + ts = OpticalSeries( + name="test_ts", + unit="unit", + external_file=["external_file"], + starting_frame=[0], + format="external", + timestamps=[1.0, 2.0], + ) + self.assertIsNone(ts.distance) + self.assertIsNone(ts.field_of_view) + self.assertIsNone(ts.orientation) + + def test_roundtrip_optional_fields(self): + """Test that OpticalSeries with optional fields set to None can be written to and read from file.""" + from datetime import datetime + import tempfile + from pynwb import NWBFile + + nwbfile = NWBFile( + session_description='test session', + identifier='TEST123', + session_start_time=datetime.now().astimezone() + ) + + # Create OpticalSeries without providing the optional fields + ts = OpticalSeries( + name="test_ts", + unit="unit", + external_file=["external_file"], + starting_frame=[0], + format="external", + timestamps=[1.0, 2.0] + ) + + nwbfile.add_acquisition(ts) + + with tempfile.NamedTemporaryFile(suffix='.nwb') as temp: + with NWBHDF5IO(temp.name, mode='w') as io: + io.write(nwbfile) + with NWBHDF5IO(temp.name, mode='r', load_namespaces=True) as io: + nwbfile_read = io.read() + ts_read = nwbfile_read.acquisition['test_ts'] + self.assertIsNone(ts_read.distance) + self.assertIsNone(ts_read.field_of_view) + self.assertIsNone(ts_read.orientation) + class TestImageSubtypes(TestCase): def test_grayscale_image(self):