From 6f5ceae572394e84d6da3170b757a14b069ab30e Mon Sep 17 00:00:00 2001
From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com>
Date: Wed, 28 Aug 2024 10:54:04 -0400
Subject: [PATCH] [Enhancement IV] Add roi response series (#19)

* add multichannel volume

* swap to datainterface

* fix import

* fix test name

* fix intercompatability

* fix light sources

* fix

* add plane segmentation stuff

* fix autogenerated arg

* variable depth volume

* swap to um

* override for roi response to avoid construct error

* add container too

* ryans suggestion

* adjust to use full list

* adjust constructor test

* reorder kwargs in mock

* adjust kwargs order in mock

* Implement lists of object references with tests

* Adjust constructor test to match

* fix outer spec to match altered one

* Update spec/ndx-microscopy.extensions.yaml

* Update spec/ndx-microscopy.extensions.yaml

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* alessandras comments and tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix import

* fix import

* fix import

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* debug

* debugs

* debugs

* debugs

* debugs

* debugs

* debugs

* debugs

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* start adding tests

* start adding tests

* complete tests

* debug

* debug

---------

Co-authored-by: CodyCBakerPhD <codycbakerphd@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
 spec/ndx-microscopy.extensions.yaml          |  28 ++++
 src/pynwb/ndx_microscopy/__init__.py         |   6 +
 src/pynwb/ndx_microscopy/testing/__init__.py |   4 +
 src/pynwb/ndx_microscopy/testing/_mock.py    |  89 ++++++++++--
 src/pynwb/tests/test_constructors.py         | 132 ++++++++++--------
 src/pynwb/tests/test_roundtrip.py            | 135 +++++++++++++------
 6 files changed, 288 insertions(+), 106 deletions(-)

diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml
index 118a9da..dd0279f 100644
--- a/spec/ndx-microscopy.extensions.yaml
+++ b/spec/ndx-microscopy.extensions.yaml
@@ -506,3 +506,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 8dfeac9..d5f989b 100644
--- a/src/pynwb/ndx_microscopy/__init__.py
+++ b/src/pynwb/ndx_microscopy/__init__.py
@@ -35,6 +35,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__ = [
     "Microscope",
     "MicroscopyLightSource",
@@ -50,4 +54,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 e27103f..848c090 100644
--- a/src/pynwb/ndx_microscopy/testing/__init__.py
+++ b/src/pynwb/ndx_microscopy/testing/__init__.py
@@ -3,6 +3,8 @@
     mock_MicroscopyLightSource,
     mock_MicroscopyOpticalChannel,
     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 862889d..cb7d919 100644
--- a/src/pynwb/ndx_microscopy/testing/_mock.py
+++ b/src/pynwb/ndx_microscopy/testing/_mock.py
@@ -11,7 +11,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:
@@ -27,7 +27,7 @@ def mock_Microscope(
 def mock_MicroscopyLightSource(
     *,
     name: Optional[str] = None,
-    description: str = "This is a mock instance of a MicroscopyLightSource type to be used for rapid testing.",
+    description: str = "A mock instance of a MicroscopyLightSource type to be used for rapid testing.",
     manufacturer: str = "A fake manufacturer of the mock light source.",
     model: str = "A fake model of the mock light source.",
     filter_description: str = "A description about the fake filter used by the mock light source.",
@@ -57,7 +57,7 @@ def mock_MicroscopyLightSource(
 def mock_MicroscopyOpticalChannel(
     *,
     name: Optional[str] = None,
-    description: str = "This is a mock instance of a MicroscopyOpticalChannel type to be used for rapid testing.",
+    description: str = "A mock instance of a MicroscopyOpticalChannel type to be used for rapid testing.",
     indicator: str = "The indicator targeted by the mock optical channel.",
     filter_description: str = "A description about the fake filter used by the mock optical channel.",
     emission_wavelength_in_nm: float = 450.0,
@@ -76,7 +76,7 @@ def mock_PlanarImagingSpace(
     *,
     microscope: ndx_microscopy.Microscope,
     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.",
@@ -98,7 +98,7 @@ def mock_VolumetricImagingSpace(
     *,
     microscope: ndx_microscopy.Microscope,
     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.",
@@ -140,7 +140,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:
@@ -166,7 +166,7 @@ def mock_PlanarMicroscopySeries(
     imaging_space: ndx_microscopy.PlanarImagingSpace,
     optical_channel: ndx_microscopy.MicroscopyOpticalChannel,
     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,
@@ -221,7 +221,7 @@ def mock_VariableDepthMicroscopySeries(
     imaging_space: ndx_microscopy.PlanarImagingSpace,
     optical_channel: ndx_microscopy.MicroscopyOpticalChannel,
     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.",
@@ -284,7 +284,7 @@ def mock_VolumetricMicroscopySeries(
     imaging_space: ndx_microscopy.VolumetricImagingSpace,
     optical_channel: ndx_microscopy.MicroscopyOpticalChannel,
     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,
@@ -339,7 +339,7 @@ def mock_MultiChannelMicroscopyVolume(
     light_sources: pynwb.base.VectorData,
     optical_channels: 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,
@@ -363,6 +363,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,
@@ -370,7 +437,7 @@ def mock_VariableDepthMultiChannelMicroscopyVolume(
     light_sources: pynwb.base.VectorData,
     optical_channels: 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 f5c29aa..bcbb3cf 100644
--- a/src/pynwb/tests/test_constructors.py
+++ b/src/pynwb/tests/test_constructors.py
@@ -1,112 +1,134 @@
 """Test in-memory Python API constructors for the ndx-microscopy extension."""
 
+import pynwb.testing.mock.ophys
 import pytest
 
+import ndx_microscopy.testing
 import pynwb
-from ndx_microscopy.testing import (
-    mock_Microscope,
-    mock_MicroscopyLightSource,
-    mock_MicroscopyOpticalChannel,
-    mock_MicroscopyPlaneSegmentation,
-    mock_MicroscopySegmentations,
-    mock_MultiChannelMicroscopyVolume,
-    mock_PlanarImagingSpace,
-    mock_PlanarMicroscopySeries,
-    mock_VariableDepthMicroscopySeries,
-    mock_VariableDepthMultiChannelMicroscopyVolume,
-    mock_VolumetricImagingSpace,
-    mock_VolumetricMicroscopySeries,
-)
 
 
 def test_constructor_microscope():
-    mock_Microscope()
+    ndx_microscopy.testing.mock_Microscope()
 
 
 def test_constructor_light_source():
-    mock_MicroscopyLightSource()
+    ndx_microscopy.testing.mock_MicroscopyLightSource()
 
 
 def test_constructor_microscopy_optical_channel():
-    mock_MicroscopyOpticalChannel()
+    ndx_microscopy.testing.mock_MicroscopyOpticalChannel()
 
 
 def test_constructor_planar_image_space():
-    microscope = mock_Microscope()
+    microscope = ndx_microscopy.testing.mock_Microscope()
 
-    mock_PlanarImagingSpace(microscope=microscope)
+    ndx_microscopy.testing.mock_PlanarImagingSpace(microscope=microscope)
 
 
 def test_constructor_volumetric_image_space():
-    microscope = mock_Microscope()
+    microscope = ndx_microscopy.testing.mock_Microscope()
 
-    mock_VolumetricImagingSpace(microscope=microscope)
+    ndx_microscopy.testing.mock_VolumetricImagingSpace(microscope=microscope)
 
 
 def test_constructor_microscopy_segmentations():
-    mock_MicroscopySegmentations()
+    ndx_microscopy.testing.mock_MicroscopySegmentations()
 
 
 def test_constructor_microscopy_plane_segmentation():
-    microscope = mock_Microscope()
-    imaging_space = mock_PlanarImagingSpace(microscope=microscope)
+    microscope = ndx_microscopy.testing.mock_Microscope()
+    imaging_space = ndx_microscopy.testing.mock_PlanarImagingSpace(microscope=microscope)
 
-    mock_MicroscopyPlaneSegmentation(imaging_space=imaging_space)
+    ndx_microscopy.testing.mock_MicroscopyPlaneSegmentation(imaging_space=imaging_space)
 
 
 def test_constructor_microscopy_image_segmentation_with_plane_segmentation():
-    microscope = mock_Microscope()
-    imaging_space = mock_PlanarImagingSpace(microscope=microscope)
+    microscope = ndx_microscopy.testing.mock_Microscope()
+    imaging_space = ndx_microscopy.testing.mock_PlanarImagingSpace(microscope=microscope)
 
-    plane_segmentation_1 = mock_MicroscopyPlaneSegmentation(
+    plane_segmentation_1 = ndx_microscopy.testing.mock_MicroscopyPlaneSegmentation(
         imaging_space=imaging_space, name="MicroscopyPlaneSegmentation1"
     )
-    plane_segmentation_2 = mock_MicroscopyPlaneSegmentation(
+    plane_segmentation_2 = ndx_microscopy.testing.mock_MicroscopyPlaneSegmentation(
         imaging_space=imaging_space, name="MicroscopyPlaneSegmentation2"
     )
     microscopy_plane_segmentations = [plane_segmentation_1, plane_segmentation_2]
 
-    mock_MicroscopySegmentations(microscopy_plane_segmentations=microscopy_plane_segmentations)
+    ndx_microscopy.testing.mock_MicroscopySegmentations(microscopy_plane_segmentations=microscopy_plane_segmentations)
 
 
 def test_constructor_planar_microscopy_series():
-    microscope = mock_Microscope()
-    light_source = mock_MicroscopyLightSource()
-    imaging_space = mock_PlanarImagingSpace(microscope=microscope)
-    optical_channel = mock_MicroscopyOpticalChannel()
+    microscope = ndx_microscopy.testing.mock_Microscope()
+    light_source = ndx_microscopy.testing.mock_MicroscopyLightSource()
+    imaging_space = ndx_microscopy.testing.mock_PlanarImagingSpace(microscope=microscope)
+    optical_channel = ndx_microscopy.testing.mock_MicroscopyOpticalChannel()
 
-    mock_PlanarMicroscopySeries(
+    ndx_microscopy.testing.mock_PlanarMicroscopySeries(
         microscope=microscope, light_source=light_source, imaging_space=imaging_space, optical_channel=optical_channel
     )
 
 
 def test_constructor_variable_depth_microscopy_series():
-    microscope = mock_Microscope()
-    light_source = mock_MicroscopyLightSource()
-    imaging_space = mock_PlanarImagingSpace(microscope=microscope)
-    optical_channel = mock_MicroscopyOpticalChannel()
+    microscope = ndx_microscopy.testing.mock_Microscope()
+    light_source = ndx_microscopy.testing.mock_MicroscopyLightSource()
+    imaging_space = ndx_microscopy.testing.mock_PlanarImagingSpace(microscope=microscope)
+    optical_channel = ndx_microscopy.testing.mock_MicroscopyOpticalChannel()
 
-    mock_VariableDepthMicroscopySeries(
+    ndx_microscopy.testing.mock_VariableDepthMicroscopySeries(
         microscope=microscope, light_source=light_source, imaging_space=imaging_space, optical_channel=optical_channel
     )
 
 
 def test_constructor_volumetric_microscopy_series():
-    microscope = mock_Microscope()
-    light_source = mock_MicroscopyLightSource()
-    imaging_space = mock_VolumetricImagingSpace(microscope=microscope)
-    optical_channel = mock_MicroscopyOpticalChannel()
+    microscope = ndx_microscopy.testing.mock_Microscope()
+    light_source = ndx_microscopy.testing.mock_MicroscopyLightSource()
+    imaging_space = ndx_microscopy.testing.mock_VolumetricImagingSpace(microscope=microscope)
+    optical_channel = ndx_microscopy.testing.mock_MicroscopyOpticalChannel()
 
-    mock_VolumetricMicroscopySeries(
+    ndx_microscopy.testing.mock_VolumetricMicroscopySeries(
         microscope=microscope, light_source=light_source, imaging_space=imaging_space, optical_channel=optical_channel
     )
 
 
+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,
+    )
+
+    ndx_microscopy.testing.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 = ndx_microscopy.testing.mock_MicroscopyResponseSeries(table_region=table_region)
+
+    ndx_microscopy.testing.mock_MicroscopyResponseSeriesContainer(
+        microscopy_response_series=[microscopy_response_series]
+    )
+
+
 def test_constructor_multi_channel_microscopy_volume():
-    microscope = mock_Microscope()
-    imaging_space = mock_VolumetricImagingSpace(microscope=microscope)
-    light_sources = [mock_MicroscopyLightSource()]
-    optical_channels = [mock_MicroscopyOpticalChannel()]
+    microscope = ndx_microscopy.testing.mock_Microscope()
+    imaging_space = ndx_microscopy.testing.mock_VolumetricImagingSpace(microscope=microscope)
+    light_sources = [ndx_microscopy.testing.mock_MicroscopyLightSource()]
+    optical_channels = [ndx_microscopy.testing.mock_MicroscopyOpticalChannel()]
 
     light_sources_used_by_volume = pynwb.base.VectorData(
         name="light_sources", description="Light sources used by this MultiChannelVolume.", data=light_sources
@@ -119,7 +141,7 @@ def test_constructor_multi_channel_microscopy_volume():
         ),
         data=optical_channels,
     )
-    mock_MultiChannelMicroscopyVolume(
+    ndx_microscopy.testing.mock_MultiChannelMicroscopyVolume(
         microscope=microscope,
         imaging_space=imaging_space,
         light_sources=light_sources_used_by_volume,
@@ -128,10 +150,10 @@ def test_constructor_multi_channel_microscopy_volume():
 
 
 def test_constructor_variable_depth_multi_channel_microscopy_volume():
-    microscope = mock_Microscope()
-    imaging_space = mock_VolumetricImagingSpace(microscope=microscope)
-    light_sources = [mock_MicroscopyLightSource()]
-    optical_channels = [mock_MicroscopyOpticalChannel()]
+    microscope = ndx_microscopy.testing.mock_Microscope()
+    imaging_space = ndx_microscopy.testing.mock_VolumetricImagingSpace(microscope=microscope)
+    light_sources = [ndx_microscopy.testing.mock_MicroscopyLightSource()]
+    optical_channels = [ndx_microscopy.testing.mock_MicroscopyOpticalChannel()]
 
     light_sources_used_by_volume = pynwb.base.VectorData(
         name="light_sources", description="Light sources used by this MultiChannelVolume.", data=light_sources
@@ -144,7 +166,7 @@ def test_constructor_variable_depth_multi_channel_microscopy_volume():
         ),
         data=optical_channels,
     )
-    mock_VariableDepthMultiChannelMicroscopyVolume(
+    ndx_microscopy.testing.mock_VariableDepthMultiChannelMicroscopyVolume(
         microscope=microscope,
         imaging_space=imaging_space,
         light_sources=light_sources_used_by_volume,
diff --git a/src/pynwb/tests/test_roundtrip.py b/src/pynwb/tests/test_roundtrip.py
index 4d9f830..80d6c54 100644
--- a/src/pynwb/tests/test_roundtrip.py
+++ b/src/pynwb/tests/test_roundtrip.py
@@ -4,20 +4,8 @@
 from pynwb.testing import TestCase as pynwb_TestCase
 from pynwb.testing.mock.file import mock_NWBFile
 
+import ndx_microscopy.testing
 import pynwb
-from ndx_microscopy.testing import (
-    mock_Microscope,
-    mock_MicroscopyLightSource,
-    mock_MicroscopyOpticalChannel,
-    mock_MicroscopyPlaneSegmentation,
-    mock_MicroscopySegmentations,
-    mock_MultiChannelMicroscopyVolume,
-    mock_PlanarImagingSpace,
-    mock_PlanarMicroscopySeries,
-    mock_VariableDepthMicroscopySeries,
-    mock_VolumetricImagingSpace,
-    mock_VolumetricMicroscopySeries,
-)
 
 
 class TestPlanarMicroscopySeriesSimpleRoundtrip(pynwb_TestCase):
@@ -32,19 +20,19 @@ def tearDown(self):
     def test_roundtrip(self):
         nwbfile = mock_NWBFile()
 
-        microscope = mock_Microscope(name="Microscope")
+        microscope = ndx_microscopy.testing.mock_Microscope(name="Microscope")
         nwbfile.add_device(devices=microscope)
 
-        light_source = mock_MicroscopyLightSource(name="MicroscopyLightSource")
+        light_source = ndx_microscopy.testing.mock_MicroscopyLightSource(name="MicroscopyLightSource")
         nwbfile.add_device(devices=light_source)
 
-        imaging_space = mock_PlanarImagingSpace(name="PlanarImagingSpace", microscope=microscope)
+        imaging_space = ndx_microscopy.testing.mock_PlanarImagingSpace(name="PlanarImagingSpace", microscope=microscope)
         nwbfile.add_lab_meta_data(lab_meta_data=imaging_space)  # Would prefer .add_imaging_spacec()
 
-        optical_channel = mock_MicroscopyOpticalChannel(name="MicroscopyOpticalChannel")
+        optical_channel = ndx_microscopy.testing.mock_MicroscopyOpticalChannel(name="MicroscopyOpticalChannel")
         nwbfile.add_lab_meta_data(lab_meta_data=optical_channel)
 
-        planar_microscopy_series = mock_PlanarMicroscopySeries(
+        planar_microscopy_series = ndx_microscopy.testing.mock_PlanarMicroscopySeries(
             name="PlanarMicroscopySeries",
             microscope=microscope,
             light_source=light_source,
@@ -80,19 +68,21 @@ def tearDown(self):
     def test_roundtrip(self):
         nwbfile = mock_NWBFile()
 
-        microscope = mock_Microscope(name="Microscope")
+        microscope = ndx_microscopy.testing.mock_Microscope(name="Microscope")
         nwbfile.add_device(devices=microscope)
 
-        light_source = mock_MicroscopyLightSource(name="MicroscopyLightSource")
+        light_source = ndx_microscopy.testing.mock_MicroscopyLightSource(name="MicroscopyLightSource")
         nwbfile.add_device(devices=light_source)
 
-        imaging_space = mock_VolumetricImagingSpace(name="VolumetricImagingSpace", microscope=microscope)
+        imaging_space = ndx_microscopy.testing.mock_VolumetricImagingSpace(
+            name="VolumetricImagingSpace", microscope=microscope
+        )
         nwbfile.add_lab_meta_data(lab_meta_data=imaging_space)  # Would prefer .add_imaging_spacec()
 
-        optical_channel = mock_MicroscopyOpticalChannel(name="MicroscopyOpticalChannel")
+        optical_channel = ndx_microscopy.testing.mock_MicroscopyOpticalChannel(name="MicroscopyOpticalChannel")
         nwbfile.add_lab_meta_data(lab_meta_data=optical_channel)
 
-        volumetric_microscopy_series = mock_VolumetricMicroscopySeries(
+        volumetric_microscopy_series = ndx_microscopy.testing.mock_VolumetricMicroscopySeries(
             name="VolumetricMicroscopySeries",
             microscope=microscope,
             light_source=light_source,
@@ -130,19 +120,19 @@ def tearDown(self):
     def test_roundtrip(self):
         nwbfile = mock_NWBFile()
 
-        microscope = mock_Microscope(name="Microscope")
+        microscope = ndx_microscopy.testing.mock_Microscope(name="Microscope")
         nwbfile.add_device(devices=microscope)
 
-        light_source = mock_MicroscopyLightSource(name="MicroscopyLightSource")
+        light_source = ndx_microscopy.testing.mock_MicroscopyLightSource(name="MicroscopyLightSource")
         nwbfile.add_device(devices=light_source)
 
-        imaging_space = mock_PlanarImagingSpace(name="PlanarImagingSpace", microscope=microscope)
+        imaging_space = ndx_microscopy.testing.mock_PlanarImagingSpace(name="PlanarImagingSpace", microscope=microscope)
         nwbfile.add_lab_meta_data(lab_meta_data=imaging_space)  # Would prefer .add_imaging_space()
 
-        optical_channel = mock_MicroscopyOpticalChannel(name="MicroscopyOpticalChannel")
+        optical_channel = ndx_microscopy.testing.mock_MicroscopyOpticalChannel(name="MicroscopyOpticalChannel")
         nwbfile.add_lab_meta_data(lab_meta_data=optical_channel)
 
-        variable_depth_microscopy_series = mock_VariableDepthMicroscopySeries(
+        variable_depth_microscopy_series = ndx_microscopy.testing.mock_VariableDepthMicroscopySeries(
             name="VariableDepthMicroscopySeries",
             microscope=microscope,
             light_source=light_source,
@@ -180,19 +170,21 @@ def tearDown(self):
     def test_roundtrip(self):
         nwbfile = mock_NWBFile()
 
-        microscope = mock_Microscope(name="Microscope")
+        microscope = ndx_microscopy.testing.mock_Microscope(name="Microscope")
         nwbfile.add_device(devices=microscope)
 
-        imaging_space = mock_VolumetricImagingSpace(name="VolumetricImagingSpace", microscope=microscope)
+        imaging_space = ndx_microscopy.testing.mock_VolumetricImagingSpace(
+            name="VolumetricImagingSpace", microscope=microscope
+        )
         nwbfile.add_lab_meta_data(lab_meta_data=imaging_space)  # Would prefer .add_imaging_space()
 
         light_sources = list()
-        light_source_0 = mock_MicroscopyLightSource(name="LightSource")
+        light_source_0 = ndx_microscopy.testing.mock_MicroscopyLightSource(name="LightSource")
         nwbfile.add_device(devices=light_source_0)
         light_sources.append(light_source_0)
 
         optical_channels = list()
-        optical_channel_0 = mock_MicroscopyOpticalChannel(name="MicroscopyOpticalChannel")
+        optical_channel_0 = ndx_microscopy.testing.mock_MicroscopyOpticalChannel(name="MicroscopyOpticalChannel")
         nwbfile.add_lab_meta_data(lab_meta_data=optical_channel_0)
         optical_channels.append(optical_channel_0)
 
@@ -209,7 +201,7 @@ def test_roundtrip(self):
             ),
             data=optical_channels,
         )
-        multi_channel_microscopy_volume = mock_MultiChannelMicroscopyVolume(
+        multi_channel_microscopy_volume = ndx_microscopy.testing.mock_MultiChannelMicroscopyVolume(
             name="MultiChannelMicroscopyVolume",
             microscope=microscope,
             imaging_space=imaging_space,
@@ -247,25 +239,25 @@ def tearDown(self):
     def test_roundtrip(self):
         nwbfile = mock_NWBFile()
 
-        microscope = mock_Microscope(name="Microscope")
+        microscope = ndx_microscopy.testing.mock_Microscope(name="Microscope")
         nwbfile.add_device(devices=microscope)
 
-        imaging_space = mock_PlanarImagingSpace(name="PlanarImagingSpace", microscope=microscope)
+        imaging_space = ndx_microscopy.testing.mock_PlanarImagingSpace(name="PlanarImagingSpace", microscope=microscope)
         nwbfile.add_lab_meta_data(lab_meta_data=imaging_space)  # Would prefer .add_imaging_space()
 
-        plane_segmentation_1 = mock_MicroscopyPlaneSegmentation(
+        plane_segmentation_1 = ndx_microscopy.testing.mock_MicroscopyPlaneSegmentation(
             imaging_space=imaging_space, name="MicroscopyPlaneSegmentation1"
         )
-        plane_segmentation_2 = mock_MicroscopyPlaneSegmentation(
+        plane_segmentation_2 = ndx_microscopy.testing.mock_MicroscopyPlaneSegmentation(
             imaging_space=imaging_space, name="MicroscopyPlaneSegmentation2"
         )
         microscopy_plane_segmentations = [plane_segmentation_1, plane_segmentation_2]
 
-        segmentations = mock_MicroscopySegmentations(
+        segmentations = ndx_microscopy.testing.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)
@@ -280,5 +272,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 = ndx_microscopy.testing.mock_Microscope(name="Microscope")
+        nwbfile.add_device(devices=microscope)
+
+        imaging_space = ndx_microscopy.testing.mock_PlanarImagingSpace(name="PlanarImagingSpace", microscope=microscope)
+        nwbfile.add_lab_meta_data(lab_meta_data=imaging_space)  # Would prefer .add_imaging_space()
+
+        microscopy_plane_segmentations = ndx_microscopy.testing.mock_MicroscopyPlaneSegmentation(
+            name="MicroscopyPlaneSegmentation", imaging_space=imaging_space
+        )
+
+        segmentations = ndx_microscopy.testing.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 = ndx_microscopy.testing.mock_MicroscopyResponseSeries(
+            name="MicroscopyResponseSeries",
+            table_region=plane_segmentation_region,
+        )
+
+        microscopy_response_series_container = ndx_microscopy.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