Skip to content

Commit

Permalink
[Enhancement I] Add multichannel volume (#13)
Browse files Browse the repository at this point in the history
* add multichannel volume

* swap to datainterface

* fix import

* fix test name

* fix intercompatability

* fix light sources

* fix

* 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

* Apply suggestions from code review

Co-authored-by: Ryan Ly <[email protected]>

* fix outer spec to match altered one (#20)

Co-authored-by: CodyCBakerPhD <[email protected]>

---------

Co-authored-by: CodyCBakerPhD <[email protected]>
Co-authored-by: Ryan Ly <[email protected]>
  • Loading branch information
3 people authored Jul 15, 2024
1 parent 6e4614a commit e5c3892
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 6 deletions.
80 changes: 79 additions & 1 deletion spec/ndx-microscopy.extensions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ groups:
- frames
- height
- width
- depth
- depths
shape:
- null
- null
Expand All @@ -258,3 +258,81 @@ 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: MultiChannelMicroscopyVolume
neurodata_type_inc: NWBDataInterface
doc: Static (not time-varying) volumetric imaging data acquired from multiple optical channels.
attributes:
- name: description
dtype: text
doc: Description of the MultiChannelVolume.
required: false
- name: unit
dtype: text
doc: Base unit of measurement for working with the data. Actual stored values are
not necessarily stored in these units. To access the data in these units,
multiply 'data' by 'conversion' and add 'offset'.
- name: conversion
dtype: float32
default_value: 1.0
doc: Scalar to multiply each element in data to convert it to the specified 'unit'.
If the data are stored in acquisition system units or other units
that require a conversion to be interpretable, multiply the data by 'conversion'
to convert the data to the specified 'unit'. e.g. if the data acquisition system
stores values in this object as signed 16-bit integers (int16 range
-32,768 to 32,767) that correspond to a 5V range (-2.5V to 2.5V), and the data
acquisition system gain is 8000X, then the 'conversion' multiplier to get from
raw data acquisition values to recorded volts is 2.5/32768/8000 = 9.5367e-9.
required: false
- name: offset
dtype: float32
default_value: 0.0
doc: Scalar to add to the data after scaling by 'conversion' to finalize its coercion
to the specified 'unit'. Two common examples of this include (a) data stored in an
unsigned type that requires a shift after scaling to re-center the data,
and (b) specialized recording devices that naturally cause a scalar offset with
respect to the true units.
required: false
datasets:
- name: data
doc: Recorded imaging data, shaped by (frame height, frame width, number of depth planes, number of optical
channels).
dtype: numeric
dims:
- height
- width
- depths
- optical_channels
shape:
- null
- null
- null
- null
- name: light_sources
doc: An ordered list of references to MicroscopyLightSource objects containing metadata about the excitation methods.
neurodata_type_inc: VectorData
dtype:
reftype: object
target_type: MicroscopyLightSource
dims:
- light_sources
shape:
- null
- name: optical_channels
doc: An ordered list of references to MicroscopyOpticalChannel objects containing metadata about the indicator and filters used to collect this data. This maps to the last dimension of `data`, i.e., the i-th MicroscopyOpticalChannel contains metadata about the indicator and filters used to collect the volume at `data[:,:,:,i]`.
neurodata_type_inc: VectorData
dtype:
reftype: object
target_type: MicroscopyOpticalChannel
dims:
- optical_channels
shape:
- null
links:
- name: microscope
doc: Link to a Microscope object containing metadata about the device used to acquire this imaging data.
target_type: Microscope
- name: imaging_space
doc: Link to VolumetricImagingSpace object containing metadata about the region of physical space this imaging data
was recorded from.
target_type: VolumetricImagingSpace
2 changes: 2 additions & 0 deletions src/pynwb/ndx_microscopy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
PlanarMicroscopySeries = get_class("PlanarMicroscopySeries", extension_name)
VariableDepthMicroscopySeries = get_class("VariableDepthMicroscopySeries", extension_name)
VolumetricMicroscopySeries = get_class("VolumetricMicroscopySeries", extension_name)
MultiChannelMicroscopyVolume = get_class("MultiChannelMicroscopyVolume", extension_name)

__all__ = [
"Microscope",
Expand All @@ -42,4 +43,5 @@
"PlanarMicroscopySeries",
"VariableDepthMicroscopySeries",
"VolumetricMicroscopySeries",
"MultiChannelMicroscopyVolume",
]
2 changes: 2 additions & 0 deletions src/pynwb/ndx_microscopy/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
mock_Microscope,
mock_MicroscopyLightSource,
mock_MicroscopyOpticalChannel,
mock_MultiChannelMicroscopyVolume,
mock_PlanarImagingSpace,
mock_PlanarMicroscopySeries,
mock_VariableDepthMicroscopySeries,
Expand All @@ -18,4 +19,5 @@
"mock_PlanarMicroscopySeries",
"mock_VariableDepthMicroscopySeries",
"mock_VolumetricMicroscopySeries",
"mock_MultiChannelMicroscopyVolume",
]
34 changes: 33 additions & 1 deletion src/pynwb/ndx_microscopy/testing/_mock.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import warnings
from typing import Optional, Tuple
from typing import List, Optional, Tuple

import numpy as np
import pynwb.base
from pynwb.testing.mock.utils import name_generator

import ndx_microscopy
Expand Down Expand Up @@ -286,3 +287,34 @@ def mock_VolumetricMicroscopySeries(
timestamps=series_timestamps,
)
return volumetric_microscopy_series


def mock_MultiChannelMicroscopyVolume(
*,
microscope: ndx_microscopy.Microscope,
imaging_space: ndx_microscopy.VolumetricImagingSpace,
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.",
data: Optional[np.ndarray] = None,
unit: str = "n.a.",
conversion: float = 1.0,
offset: float = 0.0,
) -> ndx_microscopy.MultiChannelMicroscopyVolume:
series_name = name or name_generator("MultiChannelMicroscopyVolume")
imaging_data = data if data is not None else np.ones(shape=(10, 20, 7, 3))

volumetric_microscopy_series = ndx_microscopy.MultiChannelMicroscopyVolume(
name=series_name,
description=description,
microscope=microscope,
imaging_space=imaging_space,
light_sources=light_sources,
optical_channels=optical_channels,
data=imaging_data,
unit=unit,
conversion=conversion,
offset=offset,
)
return volumetric_microscopy_series
27 changes: 27 additions & 0 deletions src/pynwb/tests/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import pytest

import pynwb
from ndx_microscopy.testing import (
mock_Microscope,
mock_MicroscopyLightSource,
mock_MicroscopyOpticalChannel,
mock_MultiChannelMicroscopyVolume,
mock_PlanarImagingSpace,
mock_PlanarMicroscopySeries,
mock_VariableDepthMicroscopySeries,
Expand Down Expand Up @@ -71,5 +73,30 @@ def test_constructor_volumetric_microscopy_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()]

light_sources_used_by_volume = pynwb.base.VectorData(
name="light_sources", description="Light sources used by this MultiChannelVolume.", data=light_sources
)
optical_channels_used_by_volume = pynwb.base.VectorData(
name="optical_channels",
description=(
"Optical channels ordered to correspond to the third axis (e.g., [0, 0, :, 0]) "
"of the data for this MultiChannelVolume."
),
data=optical_channels,
)
mock_MultiChannelMicroscopyVolume(
microscope=microscope,
imaging_space=imaging_space,
light_sources=light_sources_used_by_volume,
optical_channels=optical_channels_used_by_volume,
)


if __name__ == "__main__":
pytest.main() # Required since not a typical package structure
80 changes: 76 additions & 4 deletions src/pynwb/tests/test_roundtrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
mock_Microscope,
mock_MicroscopyLightSource,
mock_MicroscopyOpticalChannel,
mock_MultiChannelMicroscopyVolume,
mock_PlanarImagingSpace,
mock_PlanarMicroscopySeries,
mock_VariableDepthMicroscopySeries,
Expand All @@ -20,7 +21,7 @@ class TestPlanarMicroscopySeriesSimpleRoundtrip(pynwb_TestCase):
"""Simple roundtrip test for PlanarMicroscopySeries."""

def setUp(self):
self.nwbfile_path = "test.nwb"
self.nwbfile_path = "test_planar_microscopy_series_roundtrip.nwb"

def tearDown(self):
pynwb.testing.remove_test_file(self.nwbfile_path)
Expand Down Expand Up @@ -68,7 +69,7 @@ class TestVolumetricMicroscopySeriesSimpleRoundtrip(pynwb_TestCase):
"""Simple roundtrip test for VolumetricMicroscopySeries."""

def setUp(self):
self.nwbfile_path = "test.nwb"
self.nwbfile_path = "test_volumetric_microscopy_series_roundtrip.nwb"

def tearDown(self):
pynwb.testing.remove_test_file(self.nwbfile_path)
Expand Down Expand Up @@ -118,7 +119,7 @@ class TestVariableDepthMicroscopySeriesSimpleRoundtrip(pynwb_TestCase):
"""Simple roundtrip test for VariableDepthMicroscopySeries."""

def setUp(self):
self.nwbfile_path = "test.nwb"
self.nwbfile_path = "test_variable_depth_microscopy_series_roundtrip.nwb"

def tearDown(self):
pynwb.testing.remove_test_file(self.nwbfile_path)
Expand All @@ -133,7 +134,7 @@ def test_roundtrip(self):
nwbfile.add_device(devices=light_source)

imaging_space = mock_PlanarImagingSpace(name="PlanarImagingSpace", microscope=microscope)
nwbfile.add_lab_meta_data(lab_meta_data=imaging_space) # Would prefer .add_imaging_spacec()
nwbfile.add_lab_meta_data(lab_meta_data=imaging_space) # Would prefer .add_imaging_space()

optical_channel = mock_MicroscopyOpticalChannel(name="MicroscopyOpticalChannel")
nwbfile.add_lab_meta_data(lab_meta_data=optical_channel)
Expand Down Expand Up @@ -162,3 +163,74 @@ def test_roundtrip(self):
self.assertContainerEqual(
variable_depth_microscopy_series, read_nwbfile.acquisition["VariableDepthMicroscopySeries"]
)


class TestMultiChannelMicroscopyVolumeSimpleRoundtrip(pynwb_TestCase):
"""Simple roundtrip test for MultiChannelMicroscopyVolume."""

def setUp(self):
self.nwbfile_path = "test_multi_channel_microscopy_volume_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_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")
nwbfile.add_device(devices=light_source_0)
light_sources.append(light_source_0)

optical_channels = list()
optical_channel_0 = mock_MicroscopyOpticalChannel(name="MicroscopyOpticalChannel")
nwbfile.add_lab_meta_data(lab_meta_data=optical_channel_0)
optical_channels.append(optical_channel_0)

# TODO: It might be more convenient in Python to have a custom constructor that takes in a list of
# light sources and optical channels and does the VectorData wrapping internally
light_sources_used_by_volume = pynwb.base.VectorData(
name="light_sources", description="Light sources used by this MultiChannelVolume.", data=light_sources
)
optical_channels_used_by_volume = pynwb.base.VectorData(
name="optical_channels",
description=(
"Optical channels ordered to correspond to the third axis (e.g., [0, 0, :, 0]) "
"of the data for this MultiChannelVolume."
),
data=optical_channels,
)
multi_channel_microscopy_volume = mock_MultiChannelMicroscopyVolume(
name="MultiChannelMicroscopyVolume",
microscope=microscope,
imaging_space=imaging_space,
light_sources=light_sources_used_by_volume,
optical_channels=optical_channels_used_by_volume,
)
nwbfile.add_acquisition(nwbdata=multi_channel_microscopy_volume)

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(light_source_0, read_nwbfile.devices["LightSource"])

self.assertContainerEqual(imaging_space, read_nwbfile.lab_meta_data["VolumetricImagingSpace"])
self.assertContainerEqual(optical_channel_0, read_nwbfile.lab_meta_data["MicroscopyOpticalChannel"])

self.assertContainerEqual(
multi_channel_microscopy_volume, read_nwbfile.acquisition["MultiChannelMicroscopyVolume"]
)


if __name__ == "__main__":
pytest.main() # Required since not a typical package structure

0 comments on commit e5c3892

Please sign in to comment.