diff --git a/requirements-min.txt b/requirements-min.txt index 1299ede..b99b50e 100644 --- a/requirements-min.txt +++ b/requirements-min.txt @@ -1,2 +1,3 @@ pynwb -ndx-ophys-devices git+https://github.com/catalystneuro/ndx-ophys-devices.git@create_specs +git+https://github.com/catalystneuro/ndx-ophys-devices.git@main#egg=ndx-ophys-devices + diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml index 4a2e75a..d34397a 100644 --- a/spec/ndx-microscopy.extensions.yaml +++ b/spec/ndx-microscopy.extensions.yaml @@ -480,3 +480,31 @@ groups: doc: Link to VolumetricImagingSpace object containing metadata about the region of physical space this imaging data was recorded from. target_type: VolumetricImagingSpace + + + - neurodata_type_def: MicroscopyResponseSeries + neurodata_type_inc: TimeSeries + doc: ROI responses extracted from optical imaging. + datasets: + - name: data + dtype: numeric + dims: + - - number_of_frames + - number_of_rois + shape: + - - null + - null + doc: Signals from ROIs. + - name: table_region + neurodata_type_inc: DynamicTableRegion + doc: DynamicTableRegion referencing plane segmentation containing more information about the ROIs + stored in this series. + + - neurodata_type_def: MicroscopyResponseSeriesContainer + neurodata_type_inc: NWBDataInterface + default_name: MicroscopyResponseSeriesContainer + doc: A container of many MicroscopyResponseSeries. + groups: + - neurodata_type_inc: MicroscopyResponseSeries + doc: MicroscopyResponseSeries object(s) containing fluorescence data for a ROI. + quantity: '+' diff --git a/src/pynwb/ndx_microscopy/__init__.py b/src/pynwb/ndx_microscopy/__init__.py index 467f201..6b5f389 100644 --- a/src/pynwb/ndx_microscopy/__init__.py +++ b/src/pynwb/ndx_microscopy/__init__.py @@ -1,7 +1,7 @@ import os from pynwb import get_class, load_namespaces - +from pynwb.spec import NWBNamespaceBuilder try: from importlib.resources import files except ImportError: @@ -36,6 +36,10 @@ MultiChannelMicroscopyVolume = get_class("MultiChannelMicroscopyVolume", extension_name) VariableDepthMultiChannelMicroscopyVolume = get_class("VariableDepthMultiChannelMicroscopyVolume", extension_name) +MicroscopyResponseSeries = get_class("MicroscopyResponseSeries", extension_name) +MicroscopyResponseSeriesContainer = get_class("MicroscopyResponseSeriesContainer", extension_name) + + __all__ = [ "OpticalFilter", "ExcitationSource", @@ -53,4 +57,6 @@ "VolumetricMicroscopySeries", "MultiChannelMicroscopyVolume", "VariableDepthMultiChannelMicroscopyVolume", + "MicroscopyResponseSeries", + "MicroscopyResponseSeriesContainer", ] diff --git a/src/pynwb/ndx_microscopy/testing/__init__.py b/src/pynwb/ndx_microscopy/testing/__init__.py index 97ddbba..17981f7 100644 --- a/src/pynwb/ndx_microscopy/testing/__init__.py +++ b/src/pynwb/ndx_microscopy/testing/__init__.py @@ -3,6 +3,8 @@ mock_ExcitationLightPath, mock_Microscope, mock_MicroscopyPlaneSegmentation, + mock_MicroscopyResponseSeries, + mock_MicroscopyResponseSeriesContainer, mock_MicroscopySegmentations, mock_MultiChannelMicroscopyVolume, mock_PlanarImagingSpace, @@ -25,5 +27,7 @@ "mock_VariableDepthMicroscopySeries", "mock_VolumetricMicroscopySeries", "mock_MultiChannelMicroscopyVolume", + "mock_MicroscopyResponseSeries", + "mock_MicroscopyResponseSeriesContainer", "mock_VariableDepthMultiChannelMicroscopyVolume", ] diff --git a/src/pynwb/ndx_microscopy/testing/_mock.py b/src/pynwb/ndx_microscopy/testing/_mock.py index 8b9f024..a75065f 100644 --- a/src/pynwb/ndx_microscopy/testing/_mock.py +++ b/src/pynwb/ndx_microscopy/testing/_mock.py @@ -18,7 +18,7 @@ def mock_Microscope( *, name: Optional[str] = None, - description: str = "This is a mock instance of a Microscope type to be used for rapid testing.", + description: str = "A mock instance of a Microscope type to be used for rapid testing.", manufacturer: str = "A fake manufacturer of the mock microscope.", model: str = "A fake model of the mock microscope.", ) -> ndx_microscopy.Microscope: @@ -34,7 +34,7 @@ def mock_Microscope( def mock_ExcitationLightPath( *, name: Optional[str] = None, - description: str = "This is a mock instance of a ExcitationLightPath type to be used for rapid testing.", + description: str = "A mock instance of a ExcitationLightPath type to be used for rapid testing.", excitation_wavelength_in_nm: float = 500.0, excitation_source: ExcitationSource = None, excitation_filter: OpticalFilter = None, @@ -52,7 +52,7 @@ def mock_ExcitationLightPath( def mock_EmissionLightPath( *, name: Optional[str] = None, - description: str = "This is a mock instance of a EmissionLightPath type to be used for rapid testing.", + description: str = "A mock instance of a EmissionLightPath type to be used for rapid testing.", indicator: Indicator = None, photodetector: Photodetector = None, emission_filter: OpticalFilter = None, @@ -72,7 +72,7 @@ def mock_EmissionLightPath( def mock_PlanarImagingSpace( *, name: Optional[str] = None, - description: str = "This is a mock instance of a PlanarImagingSpace type to be used for rapid testing.", + description: str = "A mock instance of a PlanarImagingSpace type to be used for rapid testing.", origin_coordinates: Tuple[float, float, float] = (-1.2, -0.6, -2), grid_spacing_in_um: Tuple[float, float, float] = (20, 20), location: str = "The location targeted by the mock imaging space.", @@ -92,7 +92,7 @@ def mock_PlanarImagingSpace( def mock_VolumetricImagingSpace( *, name: Optional[str] = None, - description: str = "This is a mock instance of a VolumetricImagingSpace type to be used for rapid testing.", + description: str = "A mock instance of a VolumetricImagingSpace type to be used for rapid testing.", origin_coordinates: Tuple[float, float, float] = (-1.2, -0.6, -2), grid_spacing_in_um: Tuple[float, float, float] = (20, 20, 50), location: str = "The location targeted by the mock imaging space.", @@ -131,7 +131,7 @@ def mock_MicroscopyPlaneSegmentation( *, imaging_space: ndx_microscopy.ImagingSpace, name: Optional[str] = None, - description: str = "This is a mock instance of a MicroscopyPlaneSegmentation type to be used for rapid testing.", + description: str = "A mock instance of a MicroscopyPlaneSegmentation type to be used for rapid testing.", number_of_rois: int = 5, image_shape: Tuple[int, int] = (10, 10), ) -> ndx_microscopy.MicroscopyPlaneSegmentation: @@ -157,7 +157,7 @@ def mock_PlanarMicroscopySeries( imaging_space: ndx_microscopy.PlanarImagingSpace, emission_light_path: ndx_microscopy.EmissionLightPath, name: Optional[str] = None, - description: str = "This is a mock instance of a PlanarMicroscopySeries type to be used for rapid testing.", + description: str = "A mock instance of a PlanarMicroscopySeries type to be used for rapid testing.", data: Optional[np.ndarray] = None, unit: str = "a.u.", conversion: float = 1.0, @@ -212,7 +212,7 @@ def mock_VariableDepthMicroscopySeries( imaging_space: ndx_microscopy.PlanarImagingSpace, emission_light_path: ndx_microscopy.EmissionLightPath, name: Optional[str] = None, - description: str = "This is a mock instance of a PlanarMicroscopySeries type to be used for rapid testing.", + description: str = "A mock instance of a PlanarMicroscopySeries type to be used for rapid testing.", data: Optional[np.ndarray] = None, depth_per_frame_in_um: Optional[np.ndarray] = None, unit: str = "a.u.", @@ -275,7 +275,7 @@ def mock_VolumetricMicroscopySeries( imaging_space: ndx_microscopy.VolumetricImagingSpace, emission_light_path: ndx_microscopy.EmissionLightPath, name: Optional[str] = None, - description: str = "This is a mock instance of a VolumetricMicroscopySeries type to be used for rapid testing.", + description: str = "A mock instance of a VolumetricMicroscopySeries type to be used for rapid testing.", data: Optional[np.ndarray] = None, unit: str = "a.u.", conversion: float = 1.0, @@ -330,7 +330,7 @@ def mock_MultiChannelMicroscopyVolume( excitation_light_paths: pynwb.base.VectorData, emission_light_paths: pynwb.base.VectorData, name: Optional[str] = None, - description: str = "This is a mock instance of a MultiChannelMicroscopyVolume type to be used for rapid testing.", + description: str = "A mock instance of a MultiChannelMicroscopyVolume type to be used for rapid testing.", data: Optional[np.ndarray] = None, unit: str = "n.a.", conversion: float = 1.0, @@ -354,6 +354,73 @@ def mock_MultiChannelMicroscopyVolume( return volumetric_microscopy_series +def mock_MicroscopyResponseSeries( + *, + table_region: pynwb.core.DynamicTableRegion, + name: Optional[str] = None, + description: str = "A mock instance of a MicroscopyResponseSeries type to be used for rapid testing.", + data: Optional[np.ndarray] = None, + unit: str = "a.u.", + conversion: float = 1.0, + offset: float = 0.0, + starting_time: Optional[float] = None, + rate: Optional[float] = None, + timestamps: Optional[np.ndarray] = None, +) -> ndx_microscopy.MicroscopyResponseSeries: + series_name = name or name_generator("MicroscopyResponseSeries") + + number_of_frames = 100 + number_of_rois = len(table_region.data) + series_data = data if data is not None else np.ones(shape=(number_of_frames, number_of_rois)) + + if timestamps is None: + series_starting_time = starting_time or 0.0 + series_rate = rate or 10.0 + series_timestamps = None + else: + if starting_time is not None or rate is not None: + warnings.warn( + message=( + "Timestamps were provided in addition to either rate or starting_time! " + "Please specify only timestamps, or both starting_time and rate. Timestamps will take precedence." + ), + stacklevel=2, + ) + + series_starting_time = None + series_rate = None + series_timestamps = timestamps + + microscopy_response_series = ndx_microscopy.MicroscopyResponseSeries( + name=series_name, + description=description, + table_region=table_region, + data=series_data, + unit=unit, + conversion=conversion, + offset=offset, + starting_time=series_starting_time, + rate=series_rate, + timestamps=series_timestamps, + ) + + return microscopy_response_series + + +def mock_MicroscopyResponseSeriesContainer( + *, + microscopy_response_series: List[ndx_microscopy.MicroscopyResponseSeries], + name: Optional[str] = None, +) -> ndx_microscopy.MicroscopyResponseSeriesContainer: + container_name = name or name_generator("MicroscopyResponseSeriesContainer") + + microscopy_response_series_container = ndx_microscopy.MicroscopyResponseSeriesContainer( + name=container_name, microscopy_response_series=microscopy_response_series + ) + + return microscopy_response_series_container + + def mock_VariableDepthMultiChannelMicroscopyVolume( *, microscope: ndx_microscopy.Microscope, @@ -361,7 +428,7 @@ def mock_VariableDepthMultiChannelMicroscopyVolume( excitation_light_paths: pynwb.base.VectorData, emission_light_paths: pynwb.base.VectorData, name: Optional[str] = None, - description: str = "This is a mock instance of a MultiChannelMicroscopyVolume type to be used for rapid testing.", + description: str = "A mock instance of a MultiChannelMicroscopyVolume type to be used for rapid testing.", data: Optional[np.ndarray] = None, depth_per_frame_in_um: Optional[np.ndarray] = None, unit: str = "n.a.", diff --git a/src/pynwb/tests/test_constructors.py b/src/pynwb/tests/test_constructors.py index 0f5573e..76db432 100644 --- a/src/pynwb/tests/test_constructors.py +++ b/src/pynwb/tests/test_constructors.py @@ -1,5 +1,6 @@ """Test in-memory Python API constructors for the ndx-microscopy extension.""" +import pynwb.testing.mock.ophys import pytest import pynwb @@ -9,6 +10,8 @@ mock_Microscope, mock_MicroscopyPlaneSegmentation, mock_MicroscopySegmentations, + mock_MicroscopyResponseSeries, + mock_MicroscopyResponseSeriesContainer, mock_MultiChannelMicroscopyVolume, mock_PlanarImagingSpace, mock_PlanarMicroscopySeries, @@ -103,6 +106,40 @@ def test_constructor_volumetric_microscopy_series(): ) +def test_constructor_microscopy_response_series(): + number_of_rois = 10 + + plane_segmentation = pynwb.testing.mock.ophys.mock_PlaneSegmentation() + + table_region = pynwb.core.DynamicTableRegion( + name="table_region", + description="", + data=[x for x in range(number_of_rois)], + table=plane_segmentation, + ) + + mock_MicroscopyResponseSeries(table_region=table_region) + + +def test_constructor_microscopy_response_series_container(): + number_of_rois = 10 + + plane_segmentation = pynwb.testing.mock.ophys.mock_PlaneSegmentation() + + table_region = pynwb.core.DynamicTableRegion( + name="table_region", + description="", + data=[x for x in range(number_of_rois)], + table=plane_segmentation, + ) + + microscopy_response_series = mock_MicroscopyResponseSeries(table_region=table_region) + + mock_MicroscopyResponseSeriesContainer( + microscopy_response_series=[microscopy_response_series] + ) + + def test_constructor_multi_channel_microscopy_volume(): microscope = mock_Microscope() imaging_space = mock_VolumetricImagingSpace() diff --git a/src/pynwb/tests/test_roundtrip.py b/src/pynwb/tests/test_roundtrip.py index 99d2a16..192ec93 100644 --- a/src/pynwb/tests/test_roundtrip.py +++ b/src/pynwb/tests/test_roundtrip.py @@ -11,6 +11,7 @@ mock_Microscope, mock_MicroscopyPlaneSegmentation, mock_MicroscopySegmentations, + mock_MicroscopyResponseSeries, mock_MultiChannelMicroscopyVolume, mock_PlanarImagingSpace, mock_PlanarMicroscopySeries, @@ -18,7 +19,7 @@ mock_VolumetricImagingSpace, mock_VolumetricMicroscopySeries, ) - +from ndx_microscopy import MicroscopyResponseSeriesContainer class TestPlanarMicroscopySeriesSimpleRoundtrip(pynwb_TestCase): """Simple roundtrip test for PlanarMicroscopySeries.""" @@ -266,8 +267,8 @@ def test_roundtrip(self): segmentations = mock_MicroscopySegmentations( name="MicroscopySegmentations", microscopy_plane_segmentations=microscopy_plane_segmentations ) - processing_module = nwbfile.create_processing_module(name="ophys", description="") - processing_module.add(segmentations) + ophys_module = nwbfile.create_processing_module(name="ophys", description="") + ophys_module.add(segmentations) with pynwb.NWBHDF5IO(path=self.nwbfile_path, mode="w") as io: io.write(nwbfile) @@ -282,5 +283,68 @@ def test_roundtrip(self): self.assertContainerEqual(segmentations, read_nwbfile.processing["ophys"]["MicroscopySegmentations"]) +class TestMicroscopyResponseSeriesSimpleRoundtrip(pynwb_TestCase): + """Simple roundtrip test for MicroscopyResponseSeries.""" + + def setUp(self): + self.nwbfile_path = "test_microscopy_response_series_roundtrip.nwb" + + def tearDown(self): + pynwb.testing.remove_test_file(self.nwbfile_path) + + def test_roundtrip(self): + nwbfile = mock_NWBFile() + + microscope = mock_Microscope(name="Microscope") + nwbfile.add_device(devices=microscope) + + imaging_space = mock_PlanarImagingSpace(name="PlanarImagingSpace", microscope=microscope) + nwbfile.add_lab_meta_data(lab_meta_data=imaging_space) # Would prefer .add_imaging_space() + + microscopy_plane_segmentations = mock_MicroscopyPlaneSegmentation( + name="MicroscopyPlaneSegmentation", imaging_space=imaging_space + ) + + segmentations = mock_MicroscopySegmentations( + name="MicroscopySegmentations", microscopy_plane_segmentations=[microscopy_plane_segmentations] + ) + ophys_module = nwbfile.create_processing_module(name="ophys", description="") + ophys_module.add(segmentations) + + number_of_rois = 10 + plane_segmentation_region = pynwb.ophys.DynamicTableRegion( + name="table_region", # Name must be exactly this + description="", + data=[x for x in range(number_of_rois)], + table=microscopy_plane_segmentations, + ) + microscopy_response_series = mock_MicroscopyResponseSeries( + name="MicroscopyResponseSeries", + table_region=plane_segmentation_region, + ) + + microscopy_response_series_container = MicroscopyResponseSeriesContainer( + name="MicroscopyResponseSeriesContainer", microscopy_response_series=[microscopy_response_series] + ) + ophys_module.add(microscopy_response_series_container) + + with pynwb.NWBHDF5IO(path=self.nwbfile_path, mode="w") as io: + io.write(nwbfile) + + with pynwb.NWBHDF5IO(path=self.nwbfile_path, mode="r", load_namespaces=True) as io: + read_nwbfile = io.read() + + self.assertContainerEqual(microscope, read_nwbfile.devices["Microscope"]) + + self.assertContainerEqual(imaging_space, read_nwbfile.lab_meta_data["PlanarImagingSpace"]) + + self.assertContainerEqual(segmentations, read_nwbfile.processing["ophys"]["MicroscopySegmentations"]) + + self.assertContainerEqual( + microscopy_response_series_container, + read_nwbfile.processing["ophys"]["MicroscopyResponseSeriesContainer"], + ) + + if __name__ == "__main__": pytest.main() # Required since not a typical package structure