Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding OnePhotonSeries class #1593

Merged
merged 16 commits into from
Dec 21, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Upcoming

### Enhancements and minor changes
- Added a class and tests for the `OnePhotonSeries` new in NWB v2.6.0. @CodyCBakerPhD [#1593](https://github.com/NeurodataWithoutBorders/pynwb/pull/1593)(see also NWB Schema [#523](https://github.com/NeurodataWithoutBorders/nwb-schema/pull/523)
- `Subject.age` can be input as a `timedelta` type. @bendichter [#1590](https://github.com/NeurodataWithoutBorders/pynwb/pull/1590)
- Add `Subject.age__reference` field. @bendichter ([#1540](https://github.com/NeurodataWithoutBorders/pynwb/pull/1540))
- `IntracellularRecordingsTable.add_recording`: the `electrode` arg is now optional, and is automatically populated from the stimulus or response.
Expand Down
2 changes: 1 addition & 1 deletion src/pynwb/nwb-schema
Submodule nwb-schema updated 0 files
81 changes: 81 additions & 0 deletions src/pynwb/ophys.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,87 @@ def __init__(self, **kwargs):
setattr(self, key, val)


@register_class("OnePhotonSeries", CORE_NAMESPACE)
class OnePhotonSeries(ImageSeries):
"""Image stack recorded over time from 1-photon microscope."""

__nwbfields__ = (
"imaging_plane", "pmt_gain", "scan_line_rate", "exposure_time", "binning", "power", "intensity"
)

@docval(
*get_docval(ImageSeries.__init__, "name"), # required
{"name": "imaging_plane", "type": ImagingPlane, "doc": "Imaging plane class/pointer."}, # required
*get_docval(ImageSeries.__init__, "data", "unit", "format"),
{"name": "pmt_gain", "type": float, "doc": "Photomultiplier gain.", "default": None},
{
"name": "scan_line_rate",
"type": float,
"doc": (
"Lines imaged per second. This is also stored in /general/optophysiology but is kept "
"here as it is useful information for analysis, and so good to be stored w/ the actual data."
),
"default": None,
},
{
"name": "exposure_time",
"type": float,
"doc": "Exposure time of the sample; often the inverse of the frequency.",
"default": None,
},
{
"name": "binning",
"type": (int, "uint"),
"doc": "Amount of pixels combined into 'bins'; could be 1, 2, 4, 8, etc.",
"default": None,
},
{
"name": "power",
"type": float,
"doc": "Power of the excitation in mW, if known.",
"default": None,
},
{
"name": "intensity",
"type": float,
"doc": "Intensity of the excitation in mW/mm^2, if known.",
"default": None,
},
*get_docval(
ImageSeries.__init__,
"external_file",
"starting_frame",
"bits_per_pixel",
"dimension",
"resolution",
"conversion",
"timestamps",
"starting_time",
"rate",
"comments",
"description",
"control",
"control_description",
"device",
"offset",
)
)
def __init__(self, **kwargs):
keys_to_set = (
"imaging_plane", "pmt_gain", "scan_line_rate", "exposure_time", "binning", "power", "intensity"
)
args_to_set = popargs_to_dict(keys_to_set, kwargs)
super().__init__(**kwargs)

if args_to_set["binning"] < 0:
raise ValueError(f"Binning value must be >= 0: {args_to_set['binning']}")
if isinstance(args_to_set["binning"], int):
args_to_set["binning"] = np.uint(args_to_set["binning"])

for key, val in args_to_set.items():
setattr(self, key, val)


@register_class('TwoPhotonSeries', CORE_NAMESPACE)
class TwoPhotonSeries(ImageSeries):
"""Image stack recorded over time from 2-photon microscope."""
Expand Down
58 changes: 58 additions & 0 deletions src/pynwb/testing/mock/ophys.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
RoiResponseSeries,
OpticalChannel,
ImagingPlane,
OnePhotonSeries,
TwoPhotonSeries,
PlaneSegmentation,
ImageSegmentation,
Expand Down Expand Up @@ -63,6 +64,63 @@ def mock_ImagingPlane(
)


def mock_OnePhotonSeries(
name=None,
imaging_plane=None,
data=None,
rate=50.0,
unit="n.a.",
exposure_time=None,
binning=None,
power=None,
intensity=None,
format=None,
pmt_gain=None,
scan_line_rate=None,
external_file=None,
starting_frame=[0],
bits_per_pixel=None,
dimension=None,
resolution=-1.0,
conversion=1.0,
offset=0.0,
timestamps=None,
starting_time=None,
comments="no comments",
description="no description",
control=None,
control_description=None,
device=None,
):
return OnePhotonSeries(
name=name if name is not None else name_generator("OnePhotonSeries"),
imaging_plane=imaging_plane or mock_ImagingPlane(),
data=data if data is not None else np.ones((20, 5, 5)),
unit=unit,
exposure_time=exposure_time,
binning=binning,
power=power,
intensity=intensity,
format=format,
pmt_gain=pmt_gain,
scan_line_rate=scan_line_rate,
external_file=external_file,
starting_frame=starting_frame,
bits_per_pixel=bits_per_pixel,
dimension=dimension,
resolution=resolution,
conversion=conversion,
timestamps=timestamps,
starting_time=starting_time,
rate=rate,
comments=comments,
description=description,
control=control,
control_description=control_description,
device=device,
)


def mock_TwoPhotonSeries(
name=None,
imaging_plane=None,
Expand Down
32 changes: 32 additions & 0 deletions tests/integration/hdf5/test_ophys.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
OpticalChannel,
PlaneSegmentation,
ImageSegmentation,
OnePhotonSeries,
TwoPhotonSeries,
RoiResponseSeries,
MotionCorrection,
Expand Down Expand Up @@ -135,6 +136,37 @@ def getContainer(self, nwbfile):
return nwbfile.processing['ophys'].data_interfaces['MotionCorrection']


class TestOnePhotonSeriesIO(AcquisitionH5IOMixin, TestCase):

def setUpContainer(self):
""" Return the test OnePhotonSeries to read/write """
self.device, self.optical_channel, self.imaging_plane = make_imaging_plane()
data = np.ones((10, 2, 2))
timestamps = list(map(lambda x: x/10, range(10)))
ret = OnePhotonSeries(
name='test_2ps',
imaging_plane=self.imaging_plane,
data=data,
unit='image_unit',
format='raw',
pmt_gain=1.7,
scan_line_rate=3.4,
exposure_time=123.,
binning=2,
power=9001.,
intensity=5.,
timestamps=timestamps,
dimension=[2],
)
return ret

def addContainer(self, nwbfile):
""" Add the test OnePhotonSeries as an acquisition and add Device and ImagingPlane to the given NWBFile """
nwbfile.add_device(self.device)
nwbfile.add_imaging_plane(self.imaging_plane)
nwbfile.add_acquisition(self.container)


class TestTwoPhotonSeriesIO(AcquisitionH5IOMixin, TestCase):

def setUpContainer(self):
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/test_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pynwb.testing.mock.ophys import (
mock_ImagingPlane,
mock_OnePhotonSeries,
mock_TwoPhotonSeries,
mock_RoiResponseSeries,
mock_PlaneSegmentation,
Expand Down Expand Up @@ -52,6 +53,7 @@
@pytest.mark.parametrize(
"mock_function", [
mock_ImagingPlane,
mock_OnePhotonSeries,
mock_TwoPhotonSeries,
mock_RoiResponseSeries,
mock_PlaneSegmentation,
Expand Down
62 changes: 60 additions & 2 deletions tests/unit/test_ophys.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,19 @@
from pynwb.base import TimeSeries
from pynwb.device import Device
from pynwb.image import ImageSeries
from pynwb.ophys import (TwoPhotonSeries, RoiResponseSeries, DfOverF, Fluorescence, PlaneSegmentation,
ImageSegmentation, OpticalChannel, ImagingPlane, MotionCorrection, CorrectedImageStack)
from pynwb.ophys import (
OnePhotonSeries,
TwoPhotonSeries,
RoiResponseSeries,
DfOverF,
Fluorescence,
PlaneSegmentation,
ImageSegmentation,
OpticalChannel,
ImagingPlane,
MotionCorrection,
CorrectedImageStack
)
from pynwb.testing import TestCase


Expand Down Expand Up @@ -171,6 +182,53 @@ def test_unit_deprecated(self):
)


class OnePhotonSeriesConstructor(TestCase):

def test_init(self):
ip = create_imaging_plane()
one_photon_series = OnePhotonSeries(
name="test_one_photon_series",
unit="unit",
imaging_plane=ip,
pmt_gain=1.,
scan_line_rate=2.,
exposure_time=123.,
binning=2,
power=9001.,
intensity=5.,
external_file=["external_file"],
starting_frame=[0],
format="external",
timestamps=list(),
)
self.assertEqual(one_photon_series.name, 'test_one_photon_series')
self.assertEqual(one_photon_series.unit, 'unit')
self.assertEqual(one_photon_series.imaging_plane, ip)
self.assertEqual(one_photon_series.pmt_gain, 1.)
self.assertEqual(one_photon_series.scan_line_rate, 2.)
self.assertEqual(one_photon_series.exposure_time, 123.)
self.assertEqual(one_photon_series.binning, 2)
self.assertEqual(one_photon_series.power, 9001.)
self.assertEqual(one_photon_series.intensity, 5.)
self.assertEqual(one_photon_series.external_file, ["external_file"])
self.assertEqual(one_photon_series.starting_frame, [0])
self.assertEqual(one_photon_series.format, "external")
self.assertIsNone(one_photon_series.dimension)

def test_negative_binning_assertion(self):
ip = create_imaging_plane()

with self.assertRaisesWith(exc_type=ValueError, exc_msg="Binning value must be >= 0: -1"):
OnePhotonSeries(
name="test_one_photon_series_binning_assertion",
unit="unit",
data=np.empty(shape=(10, 100, 100)),
imaging_plane=ip,
rate=1.,
binning=-1,
)


class TwoPhotonSeriesConstructor(TestCase):

def test_init(self):
Expand Down