From 3a1e325a10f46db25ef4f20117502dca845e59df Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Mon, 1 Dec 2025 16:47:07 +0100 Subject: [PATCH 01/16] update to pynwb new Device class (old DeviceInstance) --- README.md | 14 +++---- docs/source/format.rst | 2 +- spec/ndx-microscopy.extensions.yaml | 2 +- spec/ndx-microscopy.namespace.yaml | 2 - src/pynwb/ndx_microscopy/__init__.py | 2 - src/pynwb/tests/test_roundtrip.py | 62 ++++++++++++++-------------- 6 files changed, 40 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 00ac077..5b5be35 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ classDiagram model_number : text, optional } - class DeviceInstance{ + class Device{ <> -------------------------------------- attributes @@ -73,7 +73,7 @@ classDiagram } class Microscope { - <> + <> -------------------------------------- attributes -------------------------------------- @@ -98,7 +98,7 @@ classDiagram } class ExcitationSource { - <> + <> -------------------------------------- attributes -------------------------------------- @@ -121,7 +121,7 @@ classDiagram } class OpticalFilter { - <> + <> -------------------------------------- attributes -------------------------------------- @@ -149,7 +149,7 @@ classDiagram } class DichroicMirror { - <> + <> -------------------------------------- attributes -------------------------------------- @@ -161,7 +161,7 @@ classDiagram } class Photodetector { - <> + <> -------------------------------------- attributes -------------------------------------- @@ -184,7 +184,7 @@ classDiagram } DeviceModel <|-- MicroscopeModel : extends - DeviceInstance <|-- Microscope : extends + Device <|-- Microscope : extends Microscope o--> MicroscopeModel : links MicroscopyRig o--> Microscope : links diff --git a/docs/source/format.rst b/docs/source/format.rst index 8192813..493f6bb 100644 --- a/docs/source/format.rst +++ b/docs/source/format.rst @@ -26,7 +26,7 @@ A device instance for acquiring imaging data. groups: - neurodata_type_def: Microscope - neurodata_type_inc: DeviceInstance + neurodata_type_inc: Device doc: Instance of a microscope used to acquire imaging data. attributes: - name: technique diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml index 57facb8..0e5f330 100644 --- a/spec/ndx-microscopy.extensions.yaml +++ b/spec/ndx-microscopy.extensions.yaml @@ -4,7 +4,7 @@ groups: doc: A microscope model used to acquire imaging data. - neurodata_type_def: Microscope - neurodata_type_inc: DeviceInstance + neurodata_type_inc: Device doc: Instance of a microscope used to acquire imaging data. attributes: - name: technique diff --git a/spec/ndx-microscopy.namespace.yaml b/spec/ndx-microscopy.namespace.yaml index 7a5b1dd..b1f4d04 100644 --- a/spec/ndx-microscopy.namespace.yaml +++ b/spec/ndx-microscopy.namespace.yaml @@ -21,8 +21,6 @@ namespaces: - DynamicTableRegion - namespace: ndx-ophys-devices neurodata_types: - - DeviceModel - - DeviceInstance - ExcitationSource - Indicator - OpticalFilter diff --git a/src/pynwb/ndx_microscopy/__init__.py b/src/pynwb/ndx_microscopy/__init__.py index 70a85f4..070895e 100644 --- a/src/pynwb/ndx_microscopy/__init__.py +++ b/src/pynwb/ndx_microscopy/__init__.py @@ -11,8 +11,6 @@ # NOTE: ndx-ophys-devices needs to be imported first because loading the ndx-microscopy namespace depends on # having the ndx-ophys-devices namespace loaded into the global type map. from ndx_ophys_devices import ( - DeviceModel, - DeviceInstance, ExcitationSource, Indicator, OpticalFilter, diff --git a/src/pynwb/tests/test_roundtrip.py b/src/pynwb/tests/test_roundtrip.py index befd88f..c11652c 100644 --- a/src/pynwb/tests/test_roundtrip.py +++ b/src/pynwb/tests/test_roundtrip.py @@ -51,32 +51,32 @@ def test_roundtrip(self): nwbfile = mock_NWBFile(session_start_time=datetime(2000, 1, 1, tzinfo=UTC)) microscope_model = mock_MicroscopeModel(name="MicroscopeModel") - nwbfile.add_device(devices=microscope_model) + nwbfile.add_device_model(device_models=microscope_model) microscope = mock_Microscope(name="Microscope", model=microscope_model) nwbfile.add_device(devices=microscope) excitation_source_model = mock_ExcitationSourceModel(name="ExcitationSourceModel") - nwbfile.add_device(devices=excitation_source_model) + nwbfile.add_device_model(device_models=excitation_source_model) excitation_source = mock_ExcitationSource(model=excitation_source_model) nwbfile.add_device(devices=excitation_source) excitation_filter_model = mock_OpticalFilterModel(name="OpticalFilterModel") - nwbfile.add_device(devices=excitation_filter_model) + nwbfile.add_device_model(device_models=excitation_filter_model) excitation_filter = mock_OpticalFilter(model=excitation_filter_model) nwbfile.add_device(devices=excitation_filter) dichroic_mirror_model = mock_DichroicMirrorModel(name="DichroicMirrorModel") - nwbfile.add_device(devices=dichroic_mirror_model) + nwbfile.add_device_model(device_models=dichroic_mirror_model) dichroic_mirror = mock_DichroicMirror(model=dichroic_mirror_model) nwbfile.add_device(devices=dichroic_mirror) photodetector_model = mock_PhotodetectorModel(name="PhotodetectorModel") - nwbfile.add_device(devices=photodetector_model) + nwbfile.add_device_model(device_models=photodetector_model) photodetector = mock_Photodetector(model=photodetector_model) nwbfile.add_device(devices=photodetector) emission_filter_model = mock_OpticalFilterModel(name="EmissionFilterModel") - nwbfile.add_device(devices=emission_filter_model) + nwbfile.add_device_model(device_models=emission_filter_model) emission_filter = mock_OpticalFilter(model=emission_filter_model) nwbfile.add_device(devices=emission_filter) @@ -126,32 +126,32 @@ def test_roundtrip(self): nwbfile = mock_NWBFile(session_start_time=datetime(2000, 1, 1, tzinfo=UTC)) microscope_model = mock_MicroscopeModel(name="MicroscopeModel") - nwbfile.add_device(devices=microscope_model) + nwbfile.add_device_model(device_models=microscope_model) microscope = mock_Microscope(name="Microscope", model=microscope_model) # nwbfile.add_device(devices=microscope) Skipping this line to simulate an untracked device excitation_source_model = mock_ExcitationSourceModel(name="ExcitationSourceModel") - nwbfile.add_device(devices=excitation_source_model) + nwbfile.add_device_model(device_models=excitation_source_model) excitation_source = mock_ExcitationSource(model=excitation_source_model) nwbfile.add_device(devices=excitation_source) excitation_filter_model = mock_OpticalFilterModel(name="OpticalFilterModel") - nwbfile.add_device(devices=excitation_filter_model) + nwbfile.add_device_model(device_models=excitation_filter_model) excitation_filter = mock_OpticalFilter(model=excitation_filter_model) nwbfile.add_device(devices=excitation_filter) dichroic_mirror_model = mock_DichroicMirrorModel(name="DichroicMirrorModel") - nwbfile.add_device(devices=dichroic_mirror_model) + nwbfile.add_device_model(device_models=dichroic_mirror_model) dichroic_mirror = mock_DichroicMirror(model=dichroic_mirror_model) nwbfile.add_device(devices=dichroic_mirror) photodetector_model = mock_PhotodetectorModel(name="PhotodetectorModel") - nwbfile.add_device(devices=photodetector_model) + nwbfile.add_device_model(device_models=photodetector_model) photodetector = mock_Photodetector(model=photodetector_model) nwbfile.add_device(devices=photodetector) emission_filter_model = mock_OpticalFilterModel(name="EmissionFilterModel") - nwbfile.add_device(devices=emission_filter_model) + nwbfile.add_device_model(device_models=emission_filter_model) emission_filter = mock_OpticalFilter(model=emission_filter_model) nwbfile.add_device(devices=emission_filter) @@ -194,32 +194,32 @@ def test_roundtrip(self): nwbfile = mock_NWBFile(session_start_time=datetime(2000, 1, 1, tzinfo=UTC)) microscope_model = mock_MicroscopeModel(name="MicroscopeModel") - nwbfile.add_device(devices=microscope_model) + nwbfile.add_device_model(device_models=microscope_model) microscope = mock_Microscope(name="Microscope", model=microscope_model) nwbfile.add_device(devices=microscope) excitation_source_model = mock_ExcitationSourceModel(name="ExcitationSourceModel") - nwbfile.add_device(devices=excitation_source_model) + nwbfile.add_device_model(device_models=excitation_source_model) excitation_source = mock_ExcitationSource(model=excitation_source_model) nwbfile.add_device(devices=excitation_source) excitation_filter_model = mock_OpticalFilterModel(name="OpticalFilterModel") - nwbfile.add_device(devices=excitation_filter_model) + nwbfile.add_device_model(device_models=excitation_filter_model) excitation_filter = mock_OpticalFilter(model=excitation_filter_model) nwbfile.add_device(devices=excitation_filter) dichroic_mirror_model = mock_DichroicMirrorModel(name="DichroicMirrorModel") - nwbfile.add_device(devices=dichroic_mirror_model) + nwbfile.add_device_model(device_models=dichroic_mirror_model) dichroic_mirror = mock_DichroicMirror(model=dichroic_mirror_model) nwbfile.add_device(devices=dichroic_mirror) photodetector_model = mock_PhotodetectorModel(name="PhotodetectorModel") - nwbfile.add_device(devices=photodetector_model) + nwbfile.add_device_model(device_models=photodetector_model) photodetector = mock_Photodetector(model=photodetector_model) nwbfile.add_device(devices=photodetector) emission_filter_model = mock_OpticalFilterModel(name="EmissionFilterModel") - nwbfile.add_device(devices=emission_filter_model) + nwbfile.add_device_model(device_models=emission_filter_model) emission_filter = mock_OpticalFilter(model=emission_filter_model) nwbfile.add_device(devices=emission_filter) @@ -269,32 +269,32 @@ def test_roundtrip(self): nwbfile = mock_NWBFile(session_start_time=datetime(2000, 1, 1, tzinfo=UTC)) microscope_model = mock_MicroscopeModel(name="MicroscopeModel") - nwbfile.add_device(devices=microscope_model) + nwbfile.add_device_model(device_models=microscope_model) microscope = mock_Microscope(name="Microscope", model=microscope_model) nwbfile.add_device(devices=microscope) excitation_source_model = mock_ExcitationSourceModel(name="ExcitationSourceModel") - nwbfile.add_device(devices=excitation_source_model) + nwbfile.add_device_model(device_models=excitation_source_model) excitation_source = mock_ExcitationSource(model=excitation_source_model) nwbfile.add_device(devices=excitation_source) excitation_filter_model = mock_OpticalFilterModel(name="OpticalFilterModel") - nwbfile.add_device(devices=excitation_filter_model) + nwbfile.add_device_model(device_models=excitation_filter_model) excitation_filter = mock_OpticalFilter(model=excitation_filter_model) nwbfile.add_device(devices=excitation_filter) dichroic_mirror_model = mock_DichroicMirrorModel(name="DichroicMirrorModel") - nwbfile.add_device(devices=dichroic_mirror_model) + nwbfile.add_device_model(device_models=dichroic_mirror_model) dichroic_mirror = mock_DichroicMirror(model=dichroic_mirror_model) nwbfile.add_device(devices=dichroic_mirror) photodetector_model = mock_PhotodetectorModel(name="PhotodetectorModel") - nwbfile.add_device(devices=photodetector_model) + nwbfile.add_device_model(device_models=photodetector_model) photodetector = mock_Photodetector(model=photodetector_model) nwbfile.add_device(devices=photodetector) emission_filter_model = mock_OpticalFilterModel(name="EmissionFilterModel") - nwbfile.add_device(devices=emission_filter_model) + nwbfile.add_device_model(device_models=emission_filter_model) emission_filter = mock_OpticalFilter(model=emission_filter_model) nwbfile.add_device(devices=emission_filter) @@ -364,32 +364,32 @@ def test_roundtrip(self): nwbfile = mock_NWBFile(session_start_time=datetime(2000, 1, 1, tzinfo=UTC)) microscope_model = mock_MicroscopeModel(name="MicroscopeModel") - nwbfile.add_device(devices=microscope_model) + nwbfile.add_device_model(device_models=microscope_model) microscope = mock_Microscope(name="Microscope", model=microscope_model) nwbfile.add_device(devices=microscope) excitation_source_model = mock_ExcitationSourceModel(name="ExcitationSourceModel") - nwbfile.add_device(devices=excitation_source_model) + nwbfile.add_device_model(device_models=excitation_source_model) excitation_source = mock_ExcitationSource(model=excitation_source_model) nwbfile.add_device(devices=excitation_source) excitation_filter_model = mock_OpticalFilterModel(name="OpticalFilterModel") - nwbfile.add_device(devices=excitation_filter_model) + nwbfile.add_device_model(device_models=excitation_filter_model) excitation_filter = mock_OpticalFilter(model=excitation_filter_model) nwbfile.add_device(devices=excitation_filter) dichroic_mirror_model = mock_DichroicMirrorModel(name="DichroicMirrorModel") - nwbfile.add_device(devices=dichroic_mirror_model) + nwbfile.add_device_model(device_models=dichroic_mirror_model) dichroic_mirror = mock_DichroicMirror(model=dichroic_mirror_model) nwbfile.add_device(devices=dichroic_mirror) photodetector_model = mock_PhotodetectorModel(name="PhotodetectorModel") - nwbfile.add_device(devices=photodetector_model) + nwbfile.add_device_model(device_models=photodetector_model) photodetector = mock_Photodetector(model=photodetector_model) nwbfile.add_device(devices=photodetector) emission_filter_model = mock_OpticalFilterModel(name="EmissionFilterModel") - nwbfile.add_device(devices=emission_filter_model) + nwbfile.add_device_model(device_models=emission_filter_model) emission_filter = mock_OpticalFilter(model=emission_filter_model) nwbfile.add_device(devices=emission_filter) @@ -475,7 +475,7 @@ def test_roundtrip(self): nwbfile = mock_NWBFile(session_start_time=datetime(2000, 1, 1, tzinfo=UTC)) microscope_model = mock_MicroscopeModel(name="MicroscopeModel") - nwbfile.add_device(devices=microscope_model) + nwbfile.add_device_model(device_models=microscope_model) microscope = mock_Microscope(name="Microscope", model=microscope_model) nwbfile.add_device(devices=microscope) From be0eabbb8630915b3bc62e3401c6e63bc6a36cfe Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Mon, 1 Dec 2025 16:47:21 +0100 Subject: [PATCH 02/16] Rename OpticalLens to ObjectiveLens in extensions and namespace files --- spec/ndx-microscopy.extensions.yaml | 6 +++--- spec/ndx-microscopy.namespace.yaml | 3 ++- src/pynwb/ndx_microscopy/__init__.py | 6 ++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml index 0e5f330..d9d6bb0 100644 --- a/spec/ndx-microscopy.extensions.yaml +++ b/spec/ndx-microscopy.extensions.yaml @@ -44,9 +44,9 @@ groups: target_type: OpticalFilter doc: Link to OpticalFilter object which contains metadata about the emission filter. It can be either a BandOpticalFilter (e.g., 'Bandpass', 'Bandstop', 'Longpass', 'Shortpass') or a EdgeOpticalFilter (Longpass or Shortpass). quantity: "?" - - name: optical_lens - target_type: OpticalLens - doc: Link to OpticalLens object which contains metadata about the optical lens used in the microscopy rig. + - name: objective_lens + target_type: ObjectiveLens + doc: Link to ObjectiveLens object which contains metadata about the objective lens used in the microscopy rig. quantity: "?" - neurodata_type_def: MicroscopyChannel diff --git a/spec/ndx-microscopy.namespace.yaml b/spec/ndx-microscopy.namespace.yaml index b1f4d04..e800e00 100644 --- a/spec/ndx-microscopy.namespace.yaml +++ b/spec/ndx-microscopy.namespace.yaml @@ -11,6 +11,7 @@ namespaces: - namespace: core neurodata_types: - Device + - DeviceModel - LabMetaData - NWBContainer - TimeSeries @@ -26,6 +27,6 @@ namespaces: - OpticalFilter - Photodetector - DichroicMirror - - OpticalLens + - ObjectiveLens - source: ndx-microscopy.extensions.yaml version: 0.3.0 diff --git a/src/pynwb/ndx_microscopy/__init__.py b/src/pynwb/ndx_microscopy/__init__.py index 070895e..c2fd5bb 100644 --- a/src/pynwb/ndx_microscopy/__init__.py +++ b/src/pynwb/ndx_microscopy/__init__.py @@ -16,7 +16,7 @@ OpticalFilter, Photodetector, DichroicMirror, - OpticalLens, + ObjectiveLens, ) extension_name = "ndx-microscopy" @@ -62,9 +62,7 @@ MicroscopyResponseSeriesContainer = get_class("MicroscopyResponseSeriesContainer", extension_name) __all__ = [ - "DeviceModel", - "DeviceInstance", - "OpticalLens", + "ObjectiveLens", "OpticalFilter", "ExcitationSource", "Indicator", From b637226797673cab5015cbc3366a954645bc7fd3 Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Mon, 1 Dec 2025 17:40:37 +0100 Subject: [PATCH 03/16] Add MicroscopyExperimentMetadata --- pyproject.toml | 2 +- spec/ndx-microscopy.extensions.yaml | 30 ++++++-- src/pynwb/ndx_microscopy/__init__.py | 6 ++ src/pynwb/ndx_microscopy/testing/_mock.py | 6 +- src/pynwb/tests/test_constructors.py | 41 ++++++++-- src/pynwb/tests/test_roundtrip.py | 91 +++++++++++++++++++++-- 6 files changed, 154 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7a8c38c..a88ff91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ keywords = [ dependencies = [ "pynwb>=2.8.0", "hdmf>=3.14.1", - "ndx-ophys-devices==0.2.0" + "ndx-ophys-devices==0.4.0" ] [project.urls] diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml index d9d6bb0..855efcb 100644 --- a/spec/ndx-microscopy.extensions.yaml +++ b/spec/ndx-microscopy.extensions.yaml @@ -66,10 +66,29 @@ groups: - name: emission_wavelength_in_nm dtype: float64 doc: Wavelength of the emission light in nanometers. + links: + - name: indicator + target_type: Indicator + doc: Link to Indicator object which contains metadata about the indicator used in this light path. + quantity: 1 + + - neurodata_type_def: MicroscopyExperimentMetadata + neurodata_type_inc: LabMetaData + doc: Metadata about the microscopy experiment. + name: microscopy_experiment_metadata groups: + - neurodata_type_inc: MicroscopyRig + doc: Group containing of one or more MicroscopyRig objects. + quantity: "*" + - neurodata_type_inc: ViralVector + doc: Group containing of one or more ViralVector objects. + quantity: "*" + - neurodata_type_inc: ViralVectorInjection + doc: Group containing one or more ViralVectorInjection objects. + quantity: "*" - neurodata_type_inc: Indicator - doc: Indicator object which contains metadata about the indicator used in this light path. - quantity: 1 + doc: Group containing one or more Indicator objects. + quantity: "*" - neurodata_type_def: IlluminationPattern neurodata_type_inc: NWBContainer @@ -230,10 +249,11 @@ groups: doc: Abstract class to contain imaging data acquired over time from an optical channel in a microscope while a light source illuminates the imaging space. + links: + - name: microscopy_rig + doc: Link to a MicroscopyRig object containing metadata about the microscopy rig used to acquire this imaging data. + target_type: MicroscopyRig groups: - - neurodata_type_inc: MicroscopyRig - doc: MicroscopyRig object containing metadata about the microscopy rig used to acquire this imaging data. - quantity: 1 - neurodata_type_inc: MicroscopyChannel doc: MicroscopyChannel object containing metadata about the channel used to acquire this imaging data. quantity: 1 diff --git a/src/pynwb/ndx_microscopy/__init__.py b/src/pynwb/ndx_microscopy/__init__.py index c2fd5bb..2be3337 100644 --- a/src/pynwb/ndx_microscopy/__init__.py +++ b/src/pynwb/ndx_microscopy/__init__.py @@ -13,6 +13,8 @@ from ndx_ophys_devices import ( ExcitationSource, Indicator, + ViralVector, + ViralVectorInjection, OpticalFilter, Photodetector, DichroicMirror, @@ -43,6 +45,7 @@ Microscope = get_class("Microscope", extension_name) MicroscopyRig = get_class("MicroscopyRig", extension_name) MicroscopyChannel = get_class("MicroscopyChannel", extension_name) +MicroscopyExperimentMetadata = get_class("MicroscopyExperimentMetadata", extension_name) IlluminationPattern = get_class("IlluminationPattern", extension_name) LineScan = get_class("LineScan", extension_name) PlaneAcquisition = get_class("PlaneAcquisition", extension_name) @@ -66,6 +69,8 @@ "OpticalFilter", "ExcitationSource", "Indicator", + "ViralVector", + "ViralVectorInjection", "Photodetector", "DichroicMirror", "MicroscopeModel", @@ -75,6 +80,7 @@ "PlaneAcquisition", "RandomAccessScan", "MicroscopyRig", + "MicroscopyExperimentMetadata", "ImagingSpace", "PlanarImagingSpace", "VolumetricImagingSpace", diff --git a/src/pynwb/ndx_microscopy/testing/_mock.py b/src/pynwb/ndx_microscopy/testing/_mock.py index 13bb20b..4050d5b 100644 --- a/src/pynwb/ndx_microscopy/testing/_mock.py +++ b/src/pynwb/ndx_microscopy/testing/_mock.py @@ -1,6 +1,7 @@ import warnings from typing import List, Optional, Tuple +import ndx_ophys_devices import numpy as np import pynwb.base from ndx_ophys_devices import ExcitationSource, OpticalFilter, Photodetector, DichroicMirror, Indicator @@ -10,7 +11,6 @@ mock_OpticalFilter, mock_Photodetector, mock_DichroicMirror, - mock_Indicator, ) from pynwb.testing.mock.utils import name_generator @@ -58,14 +58,14 @@ def mock_MicroscopyChannel( description: str = "A mock instance of a MicroscopyChannel type to be used for rapid testing.", excitation_wavelength_in_nm: Optional[float] = 488.0, emission_wavelength_in_nm: Optional[float] = 520.0, - indicator: Indicator = None, + indicator: ndx_ophys_devices.Indicator, ) -> ndx_microscopy.MicroscopyChannel: microscopy_channel = ndx_microscopy.MicroscopyChannel( name=name or name_generator("MicroscopyChannel"), description=description, excitation_wavelength_in_nm=excitation_wavelength_in_nm, emission_wavelength_in_nm=emission_wavelength_in_nm, - indicator=indicator or mock_Indicator(), + indicator=indicator, ) return microscopy_channel diff --git a/src/pynwb/tests/test_constructors.py b/src/pynwb/tests/test_constructors.py index 2df4f08..8838e49 100644 --- a/src/pynwb/tests/test_constructors.py +++ b/src/pynwb/tests/test_constructors.py @@ -26,6 +26,7 @@ ) from ndx_microscopy import ( + MicroscopyExperimentMetadata, Segmentation, PlanarSegmentation, VolumetricSegmentation, @@ -49,7 +50,9 @@ def test_constructor_microscope_model(): def test_constructor_microscopy_channel(): - microscopy_channel = mock_MicroscopyChannel() + from ndx_ophys_devices.testing import mock_Indicator + + microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) assert microscopy_channel.description == "A mock instance of a MicroscopyChannel type to be used for rapid testing." @@ -58,6 +61,25 @@ def test_constructor_microscopy_rig(): assert microscopy_rig.description == "A mock instance of a MicroscopyRig type to be used for rapid testing." +def test_constructor_microscopy_experiment_metadata(): + from ndx_ophys_devices.testing import mock_Indicator, mock_ViralVector, mock_ViralVectorInjection + + viral_vector = mock_ViralVector(name="ViralVector1") + viral_vector_injection = mock_ViralVectorInjection( + name="ViralVectorInjection1", + viral_vector=viral_vector, + ) + indicator = mock_Indicator(name="Indicator1", viral_vector_injection=viral_vector_injection) + microscopy_rig = mock_MicroscopyRig() + + microscopy_experiment_metadata = MicroscopyExperimentMetadata( + viral_vectors=[viral_vector], + viral_vector_injections=[viral_vector_injection], + indicators=[indicator], + microscopy_rigs=[microscopy_rig], + ) + + def test_constructor_illumination_pattern(): """Test constructor for base IlluminationPattern class.""" illumination_pattern = mock_IlluminationPattern() @@ -162,8 +184,10 @@ def test_constructor_segmentation_container(): def test_constructor_planar_microscopy_series(): + from ndx_ophys_devices.testing import mock_Indicator + + microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) microscopy_rig = mock_MicroscopyRig() - microscopy_channel = mock_MicroscopyChannel() planar_imaging_space = mock_PlanarImagingSpace() planar_microscopy_series = mock_PlanarMicroscopySeries( @@ -178,9 +202,10 @@ def test_constructor_planar_microscopy_series(): def test_constructor_multi_plane_microscopy_container(): + from ndx_ophys_devices.testing import mock_Indicator + microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) microscopy_rig = mock_MicroscopyRig() - microscopy_channel = mock_MicroscopyChannel() planar_imaging_space = mock_PlanarImagingSpace() planar_microscopy_series = mock_PlanarMicroscopySeries( @@ -196,9 +221,10 @@ def test_constructor_multi_plane_microscopy_container(): def test_constructor_multi_channel_microscopy_container(): + from ndx_ophys_devices.testing import mock_Indicator + microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) microscopy_rig = mock_MicroscopyRig() - microscopy_channel = mock_MicroscopyChannel() planar_imaging_space = mock_PlanarImagingSpace() planar_microscopy_series = mock_PlanarMicroscopySeries( microscopy_rig=microscopy_rig, @@ -213,8 +239,10 @@ def test_constructor_multi_channel_microscopy_container(): def test_constructor_volumetric_microscopy_series(): + from ndx_ophys_devices.testing import mock_Indicator + + microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) microscopy_rig = mock_MicroscopyRig() - microscopy_channel = mock_MicroscopyChannel() volumetric_imaging_space = mock_VolumetricImagingSpace() volumetric_microscopy_series = mock_VolumetricMicroscopySeries( @@ -246,9 +274,10 @@ def test_constructor_microscopy_response_series(): def test_constructor_microscopy_response_series_with_microscopy_series(): + from ndx_ophys_devices.testing import mock_Indicator + microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) microscopy_rig = mock_MicroscopyRig() - microscopy_channel = mock_MicroscopyChannel() number_of_rois = 10 planar_imaging_space = mock_PlanarImagingSpace() microscopy_series = mock_PlanarMicroscopySeries( diff --git a/src/pynwb/tests/test_roundtrip.py b/src/pynwb/tests/test_roundtrip.py index c11652c..2a122fe 100644 --- a/src/pynwb/tests/test_roundtrip.py +++ b/src/pynwb/tests/test_roundtrip.py @@ -17,6 +17,9 @@ mock_PhotodetectorModel, mock_OpticalFilterModel, mock_DichroicMirrorModel, + mock_ViralVector, + mock_ViralVectorInjection, + mock_Indicator, ) from ndx_microscopy.testing import ( @@ -35,7 +38,7 @@ mock_MicroscopyResponseSeries, ) -from ndx_microscopy import MicroscopyResponseSeriesContainer +from ndx_microscopy import MicroscopyExperimentMetadata, MicroscopyResponseSeriesContainer class TestPlanarMicroscopySeriesSimpleRoundtrip(pynwb_TestCase): @@ -89,13 +92,27 @@ def test_roundtrip(self): photodetector=photodetector, dichroic_mirror=dichroic_mirror, ) + viral_vector = mock_ViralVector(name="ViralVector1") + viral_vector_injection = mock_ViralVectorInjection( + name="ViralVectorInjection1", + viral_vector=viral_vector, + ) + indicator = mock_Indicator(name="Indicator1", viral_vector_injection=viral_vector_injection) + + microscopy_experiment_metadata = MicroscopyExperimentMetadata( + viral_vectors=[viral_vector], + viral_vector_injections=[viral_vector_injection], + indicators=[indicator], + microscopy_rigs=[microscopy_rig], + ) + nwbfile.add_lab_meta_data(microscopy_experiment_metadata) planar_imaging_space = mock_PlanarImagingSpace(name="PlanarImagingSpace") planar_microscopy_series = mock_PlanarMicroscopySeries( name="PlanarMicroscopySeries", microscopy_rig=microscopy_rig, - microscopy_channel=mock_MicroscopyChannel(name="MicroscopyChannel"), + microscopy_channel=mock_MicroscopyChannel(name="MicroscopyChannel", indicator=indicator), planar_imaging_space=planar_imaging_space, ) nwbfile.add_acquisition(planar_microscopy_series) @@ -165,13 +182,28 @@ def test_roundtrip(self): dichroic_mirror=dichroic_mirror, ) + viral_vector = mock_ViralVector(name="ViralVector1") + viral_vector_injection = mock_ViralVectorInjection( + name="ViralVectorInjection1", + viral_vector=viral_vector, + ) + indicator = mock_Indicator(name="Indicator1", viral_vector_injection=viral_vector_injection) + + microscopy_experiment_metadata = MicroscopyExperimentMetadata( + viral_vectors=[viral_vector], + viral_vector_injections=[viral_vector_injection], + indicators=[indicator], + microscopy_rigs=[microscopy_rig], + ) + nwbfile.add_lab_meta_data(microscopy_experiment_metadata) + # Create imaging space planar_imaging_space = mock_PlanarImagingSpace(name="PlanarImagingSpace") planar_microscopy_series = mock_PlanarMicroscopySeries( name="PlanarMicroscopySeries", microscopy_rig=microscopy_rig, - microscopy_channel=mock_MicroscopyChannel(name="MicroscopyChannel"), + microscopy_channel=mock_MicroscopyChannel(name="MicroscopyChannel", indicator=indicator), planar_imaging_space=planar_imaging_space, ) nwbfile.add_acquisition(planar_microscopy_series) @@ -233,12 +265,27 @@ def test_roundtrip(self): dichroic_mirror=dichroic_mirror, ) + viral_vector = mock_ViralVector(name="ViralVector1") + viral_vector_injection = mock_ViralVectorInjection( + name="ViralVectorInjection1", + viral_vector=viral_vector, + ) + indicator = mock_Indicator(name="Indicator1", viral_vector_injection=viral_vector_injection) + + microscopy_experiment_metadata = MicroscopyExperimentMetadata( + viral_vectors=[viral_vector], + viral_vector_injections=[viral_vector_injection], + indicators=[indicator], + microscopy_rigs=[microscopy_rig], + ) + nwbfile.add_lab_meta_data(microscopy_experiment_metadata) + volumetric_imaging_space = mock_VolumetricImagingSpace(name="VolumetricImagingSpace") volumetric_microscopy_series = mock_VolumetricMicroscopySeries( name="VolumetricMicroscopySeries", microscopy_rig=microscopy_rig, - microscopy_channel=mock_MicroscopyChannel(name="MicroscopyChannel"), + microscopy_channel=mock_MicroscopyChannel(name="MicroscopyChannel", indicator=indicator), volumetric_imaging_space=volumetric_imaging_space, ) nwbfile.add_acquisition(volumetric_microscopy_series) @@ -307,6 +354,20 @@ def test_roundtrip(self): photodetector=photodetector, dichroic_mirror=dichroic_mirror, ) + viral_vector = mock_ViralVector(name="ViralVector1") + viral_vector_injection = mock_ViralVectorInjection( + name="ViralVectorInjection1", + viral_vector=viral_vector, + ) + indicator = mock_Indicator(name="Indicator1", viral_vector_injection=viral_vector_injection) + + microscopy_experiment_metadata = MicroscopyExperimentMetadata( + viral_vectors=[viral_vector], + viral_vector_injections=[viral_vector_injection], + indicators=[indicator], + microscopy_rigs=[microscopy_rig], + ) + nwbfile.add_lab_meta_data(microscopy_experiment_metadata) planar_imaging_space_1 = mock_PlanarImagingSpace( name="PlanarImagingSpace_1", origin_coordinates=[0.0, 0.0, 0.0] @@ -315,7 +376,7 @@ def test_roundtrip(self): name="PlanarImagingSpace_2", origin_coordinates=[0.0, 0.0, 1.0] ) - microscopy_channel = mock_MicroscopyChannel(name="MicroscopyChannel") + microscopy_channel = mock_MicroscopyChannel(name="MicroscopyChannel", indicator=indicator) planar_microscopy_series_1 = mock_PlanarMicroscopySeries( name="PlanarMicroscopySeries_1", @@ -405,17 +466,33 @@ def test_roundtrip(self): planar_imaging_space = mock_PlanarImagingSpace(name="PlanarImagingSpace_1", origin_coordinates=[0.0, 0.0, 0.0]) + viral_vector = mock_ViralVector(name="ViralVector1") + viral_vector_injection = mock_ViralVectorInjection( + name="ViralVectorInjection1", + viral_vector=viral_vector, + ) + indicator_1 = mock_Indicator(name="Indicator1", viral_vector_injection=viral_vector_injection) + indicator_2 = mock_Indicator(name="Indicator2", viral_vector_injection=viral_vector_injection) + + microscopy_experiment_metadata = MicroscopyExperimentMetadata( + viral_vectors=[viral_vector], + viral_vector_injections=[viral_vector_injection], + indicators=[indicator_1, indicator_2], + microscopy_rigs=[microscopy_rig], + ) + nwbfile.add_lab_meta_data(microscopy_experiment_metadata) + planar_microscopy_series_1 = mock_PlanarMicroscopySeries( name="PlanarMicroscopySeries_1", microscopy_rig=microscopy_rig, - microscopy_channel=mock_MicroscopyChannel(name="MicroscopyChannel1"), + microscopy_channel=mock_MicroscopyChannel(name="MicroscopyChannel1", indicator=indicator_1), planar_imaging_space=planar_imaging_space, ) planar_microscopy_series_2 = mock_PlanarMicroscopySeries( name="PlanarMicroscopySeries_2", microscopy_rig=microscopy_rig, - microscopy_channel=mock_MicroscopyChannel(name="MicroscopyChannel2"), + microscopy_channel=mock_MicroscopyChannel(name="MicroscopyChannel2", indicator=indicator_2), planar_imaging_space=planar_imaging_space, ) From 27476b5156c3e7259c386daabe24b615a9d8b48f Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Mon, 1 Dec 2025 18:11:51 +0100 Subject: [PATCH 04/16] update documentation --- CHANGELOG.md | 21 ++++ README.md | 103 ++++++++++++++++- docs/source/format.rst | 37 ++++++- docs/source/user_guide.rst | 221 ++++++++++++++++++++++++++++++++++--- 4 files changed, 354 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d89897a..9977a8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +# v0.4.0 (Upcoming) + +## Deprecations and Changes +- **Breaking Change**: `OpticalLens`changed in `ObjectiveLens` +- **Breaking Change**: `MicroscopyChannel.indicator` changed from a nested group to a link reference +- **Breaking Change**: `MicroscopySeries.microscopy_rig` changed from a nested group to a link reference +- Updated `ndx-ophys-devices` dependency from v0.2.0 to v0.4.0 + +## Features +- Added `MicroscopyExperimentMetadata` (extends `LabMetaData`) as a centralized container for experiment metadata, including: + - `MicroscopyRig` objects + - `ViralVector` objects (from ndx-ophys-devices) + - `ViralVectorInjection` objects (from ndx-ophys-devices) + - `Indicator` objects (from ndx-ophys-devices) +- Added support for `ViralVector` and `ViralVectorInjection` imports from ndx-ophys-devices + +## Notes +- These changes improve metadata organization by centralizing all experiment-related objects in `MicroscopyExperimentMetadata` +- The use of links instead of nested groups provides better data reusability and reduces duplication +- Users should add `MicroscopyExperimentMetadata` to NWBFile using `nwbfile.add_lab_meta_data()` + # v0.3.0 (Jun 3, 2025) ## Bug Fixes diff --git a/README.md b/README.md index 5b5be35..c9f9785 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ A Neurodata Without Borders (NWB) extension for storing microscopy data and asso ## Features **Comprehensive Neurodata Types** +- Experiment metadata container: + - `MicroscopyExperimentMetadata` - Microscope and optical component metadata (integration with [ndx-ophys-devices](https://github.com/catalystneuro/ndx-ophys-devices)): - `MicroscopeModel` - `Microscope` @@ -14,6 +16,8 @@ A Neurodata Without Borders (NWB) extension for storing microscopy data and asso - `DichroicMirror` - `Photodetector` - `Indicator` + - `ViralVector` + - `ViralVectorInjection` - Microscopy channel configurations: - `MicroscopyChannel` - Imaging space definitions: @@ -266,6 +270,90 @@ classDiagram ImagingSpace *-- IlluminationPattern : contains ``` +#### Experiment Metadata Components + +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#ffffff', 'primaryBorderColor': '#144E73', 'lineColor': '#D96F32'}}}%% + +classDiagram + direction TB + + class MicroscopyExperimentMetadata { + <> + -------------------------------------- + groups + -------------------------------------- + **microscopy_rigs** : MicroscopyRig[0..*] + **viral_vectors** : ViralVector[0..*] + **viral_vector_injections** : ViralVectorInjection[0..*] + **indicators** : Indicator[0..*] + } + + class MicroscopyRig { + <> + -------------------------------------- + attributes + -------------------------------------- + description : text + -------------------------------------- + links + -------------------------------------- + microscope : Microscope + excitation_source : ExcitationSource, optional + excitation_filter : OpticalFilter, optional + dichroic_mirror : DichroicMirror, optional + photodetector : Photodetector, optional + emission_filter : OpticalFilter, optional + } + + class ViralVector { + <> + -------------------------------------- + attributes + -------------------------------------- + **construct_name** : text + titer_in_vg_per_ml : numeric, optional + manufacturer : text, optional + description : text, optional + } + class ViralVectorInjection { + <> + -------------------------------------- + attributes + -------------------------------------- + location : text, optional + hemisphere : text, optional + ap_in_mm : numeric, optional + ml_in_mm : numeric, optional + dv_in_mm : numeric, optional + pitch_in_deg : numeric, optional + yaw_in_deg : numeric, optional + roll_in_deg : numeric, optional + stereotactic_rotation_in_deg : numeric, optional + stereotactic_tilt_in_deg : numeric, optional + volume_in_uL : numeric, optional + injection_date : text, optional + **viral_vector** : ViralVector + } + class Indicator { + <> + -------------------------------------- + attributes + -------------------------------------- + **label** : text + description : text, optional + manufacturer : text, optional + **viral_vector_injection** : ViralVectorInjection, optional + } + + MicroscopyExperimentMetadata *-- MicroscopyRig : contains + MicroscopyExperimentMetadata *-- ViralVector : contains + MicroscopyExperimentMetadata *-- ViralVectorInjection : contains + MicroscopyExperimentMetadata *-- Indicator : contains + ViralVectorInjection o--> ViralVector : links + Indicator o--> ViralVectorInjection : links +``` + #### Microscopy Series and Imaging Space Components ```mermaid @@ -284,17 +372,20 @@ classDiagram **excitation_wavelength_in_nm** : float **emission_wavelength_in_nm** : float -------------------------------------- - groups + links -------------------------------------- - indicator + **indicator** : Indicator } class MicroscopySeries { <> -------------------------------------- - groups + links -------------------------------------- **microscopy_rig** : MicroscopyRig + -------------------------------------- + groups + -------------------------------------- **microscopy_channel** : MicroscopyChannel } @@ -412,9 +503,9 @@ classDiagram VolumetricMicroscopySeries *-- VolumetricImagingSpace : contains MultiPlaneMicroscopyContainer *-- PlanarMicroscopySeries : contains MultiChannelMicroscopyContainer *-- MicroscopySeries : contains - MicroscopySeries *-- MicroscopyRig : contains - MicroscopyChannel *-- MicroscopySeries : contains - MicroscopyChannel --* Indicator : contains + MicroscopySeries o--> MicroscopyRig : links + MicroscopySeries *-- MicroscopyChannel : contains + MicroscopyChannel o--> Indicator : links ``` #### Segmentation Components diff --git a/docs/source/format.rst b/docs/source/format.rst index 493f6bb..214e327 100644 --- a/docs/source/format.rst +++ b/docs/source/format.rst @@ -100,10 +100,36 @@ Represents a channel in a microscope with metadata about the indicator and wavel - name: emission_wavelength_in_nm dtype: float64 doc: Wavelength of the emission light in nanometers. + links: + - name: indicator + target_type: Indicator + doc: Link to Indicator object which contains metadata about the indicator used in this light path. + quantity: 1 + +MicroscopyExperimentMetadata +^^^^^^^^^^^^^^^^^^^^^^^^^^ +Container for centralizing all microscopy experiment metadata. + +.. code-block:: yaml + + groups: + - neurodata_type_def: MicroscopyExperimentMetadata + neurodata_type_inc: LabMetaData + doc: Metadata about the microscopy experiment. + name: microscopy_experiment_metadata groups: + - neurodata_type_inc: MicroscopyRig + doc: Group containing of one or more MicroscopyRig objects. + quantity: "*" + - neurodata_type_inc: ViralVector + doc: Group containing of one or more ViralVector objects. + quantity: "*" + - neurodata_type_inc: ViralVectorInjection + doc: Group containing one or more ViralVectorInjection objects. + quantity: "*" - neurodata_type_inc: Indicator - doc: Indicator object which contains metadata about the indicator used in this light path. - quantity: 1 + doc: Group containing one or more Indicator objects. + quantity: "*" Microscopy Series Components ------------------------ @@ -119,10 +145,11 @@ Base type for microscopy time series data. neurodata_type_inc: TimeSeries doc: Imaging data acquired over time from an optical channel in a microscope while a light source illuminates the imaging space. + links: + - name: microscopy_rig + doc: Link to a MicroscopyRig object containing metadata about the microscopy rig used to acquire this imaging data. + target_type: MicroscopyRig groups: - - neurodata_type_inc: MicroscopyRig - doc: MicroscopyRig object containing metadata about the microscopy rig used to acquire this imaging data. - quantity: 1 - neurodata_type_inc: MicroscopyChannel doc: MicroscopyChannel object containing metadata about the channel used to acquire this imaging data. quantity: 1 diff --git a/docs/source/user_guide.rst b/docs/source/user_guide.rst index d891ead..24687dc 100644 --- a/docs/source/user_guide.rst +++ b/docs/source/user_guide.rst @@ -150,7 +150,59 @@ The device components include MicroscopeModel, Microscope, and MicroscopyRig: Other optical components (filters, sources, detectors) are provided by the ndx-ophys-devices extension. -4. **MicroscopyChannel**: Defines a channel with indicator and wavelength information +4. **MicroscopyExperimentMetadata**: Container for centralizing all experiment metadata + + .. code-block:: python + + from ndx_ophys_devices import Indicator, ViralVector, ViralVectorInjection + + # Create viral vector and injection metadata + viral_vector = ViralVector( + name="viral_vector", + description="AAV viral vector for optogenetic stimulation", + construct_name="AAV-EF1a-DIO-hChR2(H134R)-EYFP", + manufacturer="Vector Manufacturer", + titer_in_vg_per_ml=1.0e12, + ) + + viral_vector_injection = ViralVectorInjection( + name="viral_vector_injection", + description="Viral vector injection for optogenetic stimulation", + location="Hippocampus", + hemisphere="right", + reference="Bregma at the cortical surface", + ap_in_mm=2.0, + ml_in_mm=1.5, + dv_in_mm=-3.0, + pitch_in_deg=0.0, + yaw_in_deg=0.0, + roll_in_deg=0.0, + stereotactic_rotation_in_deg=0.0, + stereotactic_tilt_in_deg=0.0, + volume_in_uL=0.45, + injection_date="1970-01-01T00:00:00+00:00", + viral_vector=viral_vector, + ) + + indicator = Indicator( + name="indicator", + description="Green indicator", + label="GCamp6f", + viral_vector_injection=viral_vector_injection, + ) + + # Create the experiment metadata container + microscopy_experiment_metadata = MicroscopyExperimentMetadata( + viral_vectors=[viral_vector], + viral_vector_injections=[viral_vector_injection], + indicators=[indicator], + microscopy_rigs=[microscopy_rig] + ) + + # Add to NWB file + nwbfile.add_lab_meta_data(microscopy_experiment_metadata) + +5. **MicroscopyChannel**: Defines a channel with indicator and wavelength information .. code-block:: python @@ -159,7 +211,7 @@ Other optical components (filters, sources, detectors) are provided by the ndx-o description='GCaMP6f channel', excitation_wavelength_in_nm=488.0, emission_wavelength_in_nm=520.0, - indicator=indicator # from ndx-ophys-devices + indicator=indicator # Link to indicator from MicroscopyExperimentMetadata ) Illumination Pattern Configuration @@ -332,7 +384,52 @@ Basic workflow for 2D imaging: # ... ) - # 4. Define illumination pattern + # 4. Create experiment metadata with indicators and rig + from ndx_ophys_devices import Indicator, ViralVector, ViralVectorInjection + + viral_vector = ViralVector( + name="viral_vector", + description="AAV viral vector for optogenetic stimulation", + construct_name="AAV-EF1a-DIO-hChR2(H134R)-EYFP", + manufacturer="Vector Manufacturer", + titer_in_vg_per_ml=1.0e12, + ) + + viral_vector_injection = ViralVectorInjection( + name="viral_vector_injection", + description="Viral vector injection for optogenetic stimulation", + location="Hippocampus", + hemisphere="right", + reference="Bregma at the cortical surface", + ap_in_mm=2.0, + ml_in_mm=1.5, + dv_in_mm=-3.0, + pitch_in_deg=0.0, + yaw_in_deg=0.0, + roll_in_deg=0.0, + stereotactic_rotation_in_deg=0.0, + stereotactic_tilt_in_deg=0.0, + volume_in_uL=0.45, + injection_date="1970-01-01T00:00:00+00:00", + viral_vector=viral_vector, + ) + + indicator = Indicator( + name="indicator", + description="Green indicator", + label="GCamp6f", + viral_vector_injection=viral_vector_injection, + ) + + microscopy_experiment_metadata = MicroscopyExperimentMetadata( + viral_vectors=[viral_vector], + viral_vector_injections=[viral_vector_injection], + indicators=[indicator], + microscopy_rigs=[microscopy_rig] + ) + nwbfile.add_lab_meta_data(microscopy_experiment_metadata) + + # 5. Define illumination pattern line_scan = LineScan( name='line_scanning', description='Line scanning two-photon microscopy', @@ -341,7 +438,7 @@ Basic workflow for 2D imaging: dwell_time_in_s=1.0e-6 ) - # 5. Set up imaging space with illumination pattern + # 6. Set up imaging space with illumination pattern planar_imaging_space = PlanarImagingSpace( name='cortex_plane', description='Layer 2/3 of visual cortex', @@ -354,16 +451,16 @@ Basic workflow for 2D imaging: illumination_pattern=line_scan # Include the illumination pattern ) - # 4. Create microscopy channel + # 7. Create microscopy channel microscopy_channel = MicroscopyChannel( name='gcamp_channel', description='GCaMP6f channel', excitation_wavelength_in_nm=488.0, emission_wavelength_in_nm=520.0, - indicator=indicator # from ndx-ophys-devices + indicator=indicator # Link to indicator from MicroscopyExperimentMetadata ) - # 5. Create imaging series + # 8. Create imaging series microscopy_series = PlanarMicroscopySeries( name='microscopy_series', description='Two-photon calcium imaging', @@ -414,7 +511,52 @@ Workflow for one-photon widefield imaging: # ... ) - # 4. Define illumination pattern + # 4. Create experiment metadata with indicators and rig + from ndx_ophys_devices import Indicator, ViralVector, ViralVectorInjection + + viral_vector = ViralVector( + name="viral_vector", + description="AAV viral vector for optogenetic stimulation", + construct_name="AAV-EF1a-DIO-hChR2(H134R)-EYFP", + manufacturer="Vector Manufacturer", + titer_in_vg_per_ml=1.0e12, + ) + + viral_vector_injection = ViralVectorInjection( + name="viral_vector_injection", + description="Viral vector injection for optogenetic stimulation", + location="Hippocampus", + hemisphere="right", + reference="Bregma at the cortical surface", + ap_in_mm=2.0, + ml_in_mm=1.5, + dv_in_mm=-3.0, + pitch_in_deg=0.0, + yaw_in_deg=0.0, + roll_in_deg=0.0, + stereotactic_rotation_in_deg=0.0, + stereotactic_tilt_in_deg=0.0, + volume_in_uL=0.45, + injection_date="1970-01-01T00:00:00+00:00", + viral_vector=viral_vector, + ) + + indicator = Indicator( + name="indicator", + description="Green indicator", + label="GCamp6f", + viral_vector_injection=viral_vector_injection, + ) + + microscopy_experiment_metadata = MicroscopyExperimentMetadata( + viral_vectors=[viral_vector], + viral_vector_injections=[viral_vector_injection], + indicators=[indicator], + microscopy_rigs=[microscopy_rig] + ) + nwbfile.add_lab_meta_data(microscopy_experiment_metadata) + + # 5. Define illumination pattern plane_acquisition = PlaneAcquisition( name='plane_acquisition', description='Widefield fluorescence imaging', @@ -422,7 +564,7 @@ Workflow for one-photon widefield imaging: plane_rate_in_Hz=30.0 ) - # 5. Set up imaging space with illumination pattern + # 6. Set up imaging space with illumination pattern planar_imaging_space = PlanarImagingSpace( name='hippo_plane', description='CA1 region of hippocampus', @@ -435,16 +577,16 @@ Workflow for one-photon widefield imaging: illumination_pattern=plane_acquisition ) - # 6. Create microscopy channel + # 7. Create microscopy channel microscopy_channel = MicroscopyChannel( name='gcamp_channel', description='GCaMP6f channel', excitation_wavelength_in_nm=470.0, emission_wavelength_in_nm=520.0, - indicator=indicator # from ndx-ophys-devices + indicator=indicator # Link to indicator from MicroscopyExperimentMetadata ) - # 5. Create imaging series + # 8. Create imaging series microscopy_series = PlanarMicroscopySeries( name='imaging_data', description='One-photon calcium imaging', @@ -495,7 +637,52 @@ Workflow for volumetric imaging with targeted scanning: # ... ) - # 4. Define illumination pattern + # 4. Create experiment metadata with indicators and rig + from ndx_ophys_devices import Indicator, ViralVector, ViralVectorInjection + + viral_vector = ViralVector( + name="viral_vector", + description="AAV viral vector for optogenetic stimulation", + construct_name="AAV-EF1a-DIO-hChR2(H134R)-EYFP", + manufacturer="Vector Manufacturer", + titer_in_vg_per_ml=1.0e12, + ) + + viral_vector_injection = ViralVectorInjection( + name="viral_vector_injection", + description="Viral vector injection for optogenetic stimulation", + location="Hippocampus", + hemisphere="right", + reference="Bregma at the cortical surface", + ap_in_mm=2.0, + ml_in_mm=1.5, + dv_in_mm=-3.0, + pitch_in_deg=0.0, + yaw_in_deg=0.0, + roll_in_deg=0.0, + stereotactic_rotation_in_deg=0.0, + stereotactic_tilt_in_deg=0.0, + volume_in_uL=0.45, + injection_date="1970-01-01T00:00:00+00:00", + viral_vector=viral_vector, + ) + + indicator = Indicator( + name="indicator", + description="Green indicator", + label="GCamp6f", + viral_vector_injection=viral_vector_injection, + ) + + microscopy_experiment_metadata = MicroscopyExperimentMetadata( + viral_vectors=[viral_vector], + viral_vector_injections=[viral_vector_injection], + indicators=[indicator], + microscopy_rigs=[microscopy_rig] + ) + nwbfile.add_lab_meta_data(microscopy_experiment_metadata) + + # 5. Define illumination pattern random_access_scan = RandomAccessScan( name='random_access', description='Targeted imaging of specific neurons', @@ -504,7 +691,7 @@ Workflow for volumetric imaging with targeted scanning: scanning_pattern='spiral' ) - # 5. Set up volumetric space with illumination pattern + # 6. Set up volumetric space with illumination pattern volumetric_imaging_space = VolumetricImagingSpace( name='cortex_volume', description='Visual cortex volume', @@ -517,16 +704,16 @@ Workflow for volumetric imaging with targeted scanning: illumination_pattern=random_access_scan ) - # 6. Create microscopy channel + # 7. Create microscopy channel microscopy_channel = MicroscopyChannel( name='gcamp_channel', description='GCaMP6f channel', excitation_wavelength_in_nm=920.0, emission_wavelength_in_nm=520.0, - indicator=indicator # from ndx-ophys-devices + indicator=indicator # Link to indicator from MicroscopyExperimentMetadata ) - # 5. Create volumetric series + # 8. Create volumetric series volume_series = VolumetricMicroscopySeries( name='volume_data', microscopy_rig=microscopy_rig, From b126a1d25c6c5353294e5200f6abc220b300493d Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 2 Dec 2025 15:14:41 +0100 Subject: [PATCH 05/16] Eliminate the reference to one ImagingSpace in the `SegmentationContainer" doc --- docs/source/format.rst | 2 +- spec/ndx-microscopy.extensions.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/format.rst b/docs/source/format.rst index 214e327..d69b1ae 100644 --- a/docs/source/format.rst +++ b/docs/source/format.rst @@ -587,7 +587,7 @@ Container for multiple segmentations. doc: A container of many Segmentation objects. groups: - neurodata_type_inc: Segmentation - doc: Results from image segmentation of a specific imaging space. + doc: Results from image segmentation. quantity: "+" SummaryImage diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml index 855efcb..0dde8d6 100644 --- a/spec/ndx-microscopy.extensions.yaml +++ b/spec/ndx-microscopy.extensions.yaml @@ -342,7 +342,7 @@ groups: ROI names should remain consistent between them. groups: - neurodata_type_inc: Segmentation - doc: Results from image segmentation of a specific imaging space. + doc: Results from image segmentation. quantity: "+" - neurodata_type_def: SummaryImage From 47bc9fa7f49a08fe4cb63bb04b1a6d4b8a08c05f Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 2 Dec 2025 15:45:15 +0100 Subject: [PATCH 06/16] update doc --- docs/source/format.rst | 16 ++++++++++------ spec/ndx-microscopy.extensions.yaml | 24 ++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/source/format.rst b/docs/source/format.rst index d69b1ae..a36c830 100644 --- a/docs/source/format.rst +++ b/docs/source/format.rst @@ -467,7 +467,7 @@ Base type for segmentation data. groups: - neurodata_type_def: Segmentation neurodata_type_inc: DynamicTable - doc: Abstract class to contain the results from image segmentation of a specific imaging space. + doc: Abstract class to contain the spatial components resulting from image segmentation of a specific imaging space. attributes: - name: description dtype: text @@ -486,7 +486,7 @@ For 2D segmentation data. groups: - neurodata_type_def: PlanarSegmentation neurodata_type_inc: Segmentation - doc: Results from image segmentation of a specific planar imaging space. + doc: ROI spatial components resulting from image segmentation of a specific planar imaging space. datasets: - name: image_mask neurodata_type_inc: VectorData @@ -532,7 +532,7 @@ For 3D segmentation data. groups: - neurodata_type_def: VolumetricSegmentation neurodata_type_inc: Segmentation - doc: Results from image segmentation of a specific volumetric imaging space. + doc: ROI spatial components resulting from image segmentation of a specific volumetric imaging space. datasets: - name: volume_mask neurodata_type_inc: VectorData @@ -630,7 +630,10 @@ For extracted ROI responses. groups: - neurodata_type_def: MicroscopyResponseSeries neurodata_type_inc: TimeSeries - doc: ROI responses extracted from optical imaging. + doc: + ROI responses extracted from imaging data, linked in the microscopy_series field. + This object contains the temporal components from multiple ROIs, + that can result from different processing steps, e.g., raw, deconvolved, or denoised fluorescence traces. datasets: - name: data dtype: numeric @@ -643,8 +646,9 @@ For extracted ROI responses. doc: Signals from ROIs. - name: rois neurodata_type_inc: DynamicTableRegion - doc: DynamicTableRegion referencing segmentation containing more information about the ROIs - stored in this series. + doc: + DynamicTableRegion referencing Segmentation table containing information about the ROIs + spatial components. links: - name: microscopy_series doc: Link to a MicroscopySeries object containing the imaging data this response series is derived from. diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml index 0dde8d6..fc12295 100644 --- a/spec/ndx-microscopy.extensions.yaml +++ b/spec/ndx-microscopy.extensions.yaml @@ -332,14 +332,7 @@ groups: - neurodata_type_def: SegmentationContainer neurodata_type_inc: NWBDataInterface default_name: SegmentationContainer - doc: - Stores pixels in an image that represent different regions of interest (ROIs) - or masks. All segmentation for a given imaging space is stored together, with - storage for multiple imaging space (masks) supported. Each ROI is stored in its - own subgroup, with the ROI group containing a 2D mask or a list of pixels - that make up this mask. Segments can also be used for masking neuropil. If segmentation - is allowed to change with time, a new imaging plane (or module) is required and - ROI names should remain consistent between them. + doc: A container for many Segmentation objects. groups: - neurodata_type_inc: Segmentation doc: Results from image segmentation. @@ -371,7 +364,7 @@ groups: - neurodata_type_def: Segmentation neurodata_type_inc: DynamicTable - doc: Abstract class to contain the results from image segmentation of a specific imaging space. + doc: Abstract class to contain the spatial components resulting from image segmentation of a specific imaging space. attributes: - name: description dtype: text @@ -383,7 +376,7 @@ groups: - neurodata_type_def: PlanarSegmentation neurodata_type_inc: Segmentation - doc: Results from image segmentation of a specific planar imaging space. + doc: ROI spatial components resulting from image segmentation of a specific planar imaging space. datasets: - name: image_mask neurodata_type_inc: VectorData @@ -425,7 +418,7 @@ groups: - neurodata_type_def: VolumetricSegmentation neurodata_type_inc: Segmentation - doc: Results from image segmentation of a specific volumetric imaging space. + doc: ROI spatial components resulting from image segmentation of a specific volumetric imaging space. datasets: - name: volume_mask neurodata_type_inc: VectorData @@ -472,7 +465,10 @@ groups: - neurodata_type_def: MicroscopyResponseSeries neurodata_type_inc: TimeSeries - doc: ROI responses extracted from optical imaging. + doc: + ROI responses extracted from imaging data, linked in the microscopy_series field. + This object contains the temporal components from multiple ROIs, + that can result from different processing steps, e.g., raw, deconvolved, or denoised fluorescence traces. datasets: - name: data dtype: numeric @@ -486,8 +482,8 @@ groups: - name: rois neurodata_type_inc: DynamicTableRegion doc: - DynamicTableRegion referencing segmentation containing more information about the ROIs - stored in this series. + DynamicTableRegion referencing Segmentation table containing information about the ROIs + spatial components. links: - name: microscopy_series doc: Link to a MicroscopySeries object containing the imaging data this response series is derived from. From 340cf8d32e41334e1affef5d0bbabab38674f9bd Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 2 Dec 2025 15:47:47 +0100 Subject: [PATCH 07/16] add TODO --- spec/ndx-microscopy.extensions.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml index fc12295..39a0df6 100644 --- a/spec/ndx-microscopy.extensions.yaml +++ b/spec/ndx-microscopy.extensions.yaml @@ -498,3 +498,4 @@ groups: - neurodata_type_inc: MicroscopyResponseSeries doc: MicroscopyResponseSeries object(s) containing fluorescence data for a ROI. quantity: "+" +#TODO add static images From 3d41923f3640d4ceb93f64982a545ec69580c6d0 Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 2 Dec 2025 16:02:32 +0100 Subject: [PATCH 08/16] ruff fixes --- src/pynwb/ndx_microscopy/testing/_mock.py | 3 +-- src/pynwb/tests/test_constructors.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pynwb/ndx_microscopy/testing/_mock.py b/src/pynwb/ndx_microscopy/testing/_mock.py index 4050d5b..0edbd0a 100644 --- a/src/pynwb/ndx_microscopy/testing/_mock.py +++ b/src/pynwb/ndx_microscopy/testing/_mock.py @@ -3,8 +3,7 @@ import ndx_ophys_devices import numpy as np -import pynwb.base -from ndx_ophys_devices import ExcitationSource, OpticalFilter, Photodetector, DichroicMirror, Indicator +from ndx_ophys_devices import ExcitationSource, OpticalFilter, Photodetector, DichroicMirror from ndx_ophys_devices.testing import ( mock_ExcitationSource, diff --git a/src/pynwb/tests/test_constructors.py b/src/pynwb/tests/test_constructors.py index 8838e49..9db9ebc 100644 --- a/src/pynwb/tests/test_constructors.py +++ b/src/pynwb/tests/test_constructors.py @@ -72,7 +72,7 @@ def test_constructor_microscopy_experiment_metadata(): indicator = mock_Indicator(name="Indicator1", viral_vector_injection=viral_vector_injection) microscopy_rig = mock_MicroscopyRig() - microscopy_experiment_metadata = MicroscopyExperimentMetadata( + _ = MicroscopyExperimentMetadata( viral_vectors=[viral_vector], viral_vector_injections=[viral_vector_injection], indicators=[indicator], From 9cd37a8435118bb3d3c257e642983f0726ed6ced Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 2 Dec 2025 16:09:02 +0100 Subject: [PATCH 09/16] Fix import for DynamicTableRegion in mock.py and update type hint in mock_MicroscopyResponseSeries --- src/pynwb/ndx_microscopy/testing/_mock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pynwb/ndx_microscopy/testing/_mock.py b/src/pynwb/ndx_microscopy/testing/_mock.py index 0edbd0a..538b09f 100644 --- a/src/pynwb/ndx_microscopy/testing/_mock.py +++ b/src/pynwb/ndx_microscopy/testing/_mock.py @@ -13,7 +13,7 @@ ) from pynwb.testing.mock.utils import name_generator - +from pynwb.core import DynamicTableRegion import ndx_microscopy @@ -478,7 +478,7 @@ def mock_VolumetricMicroscopySeries( def mock_MicroscopyResponseSeries( *, - rois: pynwb.core.DynamicTableRegion, + rois: 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, From a2b5c17122ba664e0b06248dfab2433f5593d0a8 Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 2 Dec 2025 18:13:49 +0100 Subject: [PATCH 10/16] add static images representation --- docs/source/format.rst | 18 +- spec/ndx-microscopy.extensions.yaml | 74 +++++++- spec/ndx-microscopy.namespace.yaml | 1 + src/pynwb/ndx_microscopy/__init__.py | 6 + src/pynwb/ndx_microscopy/testing/__init__.py | 4 + src/pynwb/ndx_microscopy/testing/_mock.py | 62 ++++++- src/pynwb/tests/test_constructors.py | 78 +++++++- src/pynwb/tests/test_roundtrip.py | 177 +++++++++++++++++++ 8 files changed, 403 insertions(+), 17 deletions(-) diff --git a/docs/source/format.rst b/docs/source/format.rst index a36c830..a7ff3ee 100644 --- a/docs/source/format.rst +++ b/docs/source/format.rst @@ -223,12 +223,16 @@ Container for multiple PlanarMicroscopySeries. - neurodata_type_def: MultiPlaneMicroscopyContainer neurodata_type_inc: NWBDataInterface default_name: MultiPlaneMicroscopyContainer - doc: Imaging data acquired over several depths, regularly or irregularly spaced; for instance, when using an - electrically tunable lens. Each depth scan is stored in a separate PlanarMicroscopySeries object. + doc: + Imaging data acquired over several depths, regularly or irregularly spaced; for instance, when using an + electrically tunable lens. Each depth scan is stored in a separate PlanarMicroscopySeries or PlanarMicroscopyStaticImage object. groups: - neurodata_type_inc: PlanarMicroscopySeries doc: PlanarMicroscopySeries object(s) containing imaging data for a single depth scan. - quantity: "+" + quantity: "*" + - neurodata_type_inc: PlanarMicroscopyStaticImage + doc: PlanarMicroscopyStaticImage object(s) containing imaging data for a single depth scan. + quantity: "*" MultiChannelMicroscopyContainer ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -242,11 +246,15 @@ Container for multiple PlanerMicroscopySeries or VolumetricMicroscopySeries acqu default_name: MultiChannelMicroscopyContainer doc: Imaging data acquired over several channels; for instance, when using multiple excitation wavelengths - or multiple indicators. Each channel is stored in a separate PlanarMicroscopySeries or VolumetricMicroscopySeries object. + or multiple indicators. Each channel is stored in a separate PlanarMicroscopySeries, VolumetricMicroscopySeries, + PlanarMicroscopyStaticImage or VolumetricMicroscopyStaticImage. groups: - neurodata_type_inc: MicroscopySeries doc: MicroscopySeries object containing imaging data for a single channel scan. - quantity: "+" + quantity: "*" + - neurodata_type_inc: MicroscopyStaticImage + doc: MicroscopyStaticImage object containing imaging data for a single channel scan. + quantity: "*" Illumination Pattern Components -------------------------- diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml index 39a0df6..0260123 100644 --- a/spec/ndx-microscopy.extensions.yaml +++ b/spec/ndx-microscopy.extensions.yaml @@ -244,6 +244,65 @@ groups: - - 3 quantity: "?" + - neurodata_type_def: MicroscopyStaticImage + neurodata_type_inc: Image --> bisogna usare per forza container + doc: + Abstract class to contain static imaging data acquired from an optical channel in a microscope while a light source illuminates the + imaging space. + links: + - name: microscopy_rig + doc: Link to a MicroscopyRig object containing metadata about the microscopy rig used to acquire this imaging data. + target_type: MicroscopyRig + groups: + - neurodata_type_inc: MicroscopyChannel + doc: MicroscopyChannel object containing metadata about the channel used to acquire this imaging data. + quantity: 1 + + - neurodata_type_def: PlanarMicroscopyStaticImage + neurodata_type_inc: MicroscopyStaticImage + doc: + Static imaging data acquired from an optical channel in a microscope while a light source illuminates a + planar imaging space. + datasets: + - name: data + doc: Recorded imaging data, shaped by (frame height, frame width). + dtype: numeric + dims: + - height + - width + shape: + - null + - null + groups: + - neurodata_type_inc: PlanarImagingSpace + doc: + PlanarImagingSpace object containing metadata about the region of physical space this imaging data + was recorded from. + + - neurodata_type_def: VolumetricMicroscopyStaticImage + neurodata_type_inc: MicroscopyStaticImage + doc: + Static volumetric imaging data acquired from an optical channel in a microscope while a light source + illuminates a volumetric imaging space. + Assumes the number of depth scans used to construct the volume is regular. + datasets: + - name: data + doc: Recorded imaging data, shaped by (frame height, frame width, number of depth planes). + dtype: numeric + dims: + - height + - width + - depths + shape: + - null + - null + - null + groups: + - neurodata_type_inc: VolumetricImagingSpace + doc: + VolumetricImagingSpace object containing metadata about the region of physical space this imaging data + was recorded from. + - neurodata_type_def: MicroscopySeries neurodata_type_inc: TimeSeries doc: @@ -312,22 +371,29 @@ groups: default_name: MultiPlaneMicroscopyContainer doc: Imaging data acquired over several depths, regularly or irregularly spaced; for instance, when using an - electrically tunable lens. Each depth scan is stored in a separate PlanarMicroscopySeries object. + electrically tunable lens. Each depth scan is stored in a separate PlanarMicroscopySeries or PlanarMicroscopyStaticImage object. groups: - neurodata_type_inc: PlanarMicroscopySeries doc: PlanarMicroscopySeries object(s) containing imaging data for a single depth scan. - quantity: "+" + quantity: "*" + - neurodata_type_inc: PlanarMicroscopyStaticImage + doc: PlanarMicroscopyStaticImage object(s) containing imaging data for a single depth scan. + quantity: "*" - neurodata_type_def: MultiChannelMicroscopyContainer neurodata_type_inc: NWBDataInterface default_name: MultiChannelMicroscopyContainer doc: Imaging data acquired over several channels; for instance, when using multiple excitation wavelengths - or multiple indicators. Each channel is stored in a separate PlanarMicroscopySeries or VolumetricMicroscopySeries object. + or multiple indicators. Each channel is stored in a separate PlanarMicroscopySeries, VolumetricMicroscopySeries, + PlanarMicroscopyStaticImage or VolumetricMicroscopyStaticImage. groups: - neurodata_type_inc: MicroscopySeries doc: MicroscopySeries object containing imaging data for a single channel scan. - quantity: "+" + quantity: "*" + - neurodata_type_inc: MicroscopyStaticImage + doc: MicroscopyStaticImage object containing imaging data for a single channel scan. + quantity: "*" - neurodata_type_def: SegmentationContainer neurodata_type_inc: NWBDataInterface diff --git a/spec/ndx-microscopy.namespace.yaml b/spec/ndx-microscopy.namespace.yaml index e800e00..01c7b9a 100644 --- a/spec/ndx-microscopy.namespace.yaml +++ b/spec/ndx-microscopy.namespace.yaml @@ -20,6 +20,7 @@ namespaces: - VectorData - VectorIndex - DynamicTableRegion + - ImageSeries - namespace: ndx-ophys-devices neurodata_types: - ExcitationSource diff --git a/src/pynwb/ndx_microscopy/__init__.py b/src/pynwb/ndx_microscopy/__init__.py index 2be3337..e6bc951 100644 --- a/src/pynwb/ndx_microscopy/__init__.py +++ b/src/pynwb/ndx_microscopy/__init__.py @@ -55,6 +55,9 @@ PlanarImagingSpace = get_class("PlanarImagingSpace", extension_name) VolumetricImagingSpace = get_class("VolumetricImagingSpace", extension_name) +MicroscopyStaticImage = get_class("MicroscopyStaticImage", extension_name) +PlanarMicroscopyStaticImage = get_class("PlanarMicroscopyStaticImage", extension_name) +VolumetricMicroscopyStaticImage = get_class("VolumetricMicroscopyStaticImage", extension_name) MicroscopySeries = get_class("MicroscopySeries", extension_name) PlanarMicroscopySeries = get_class("PlanarMicroscopySeries", extension_name) VolumetricMicroscopySeries = get_class("VolumetricMicroscopySeries", extension_name) @@ -91,6 +94,9 @@ "MicroscopySeries", "PlanarMicroscopySeries", "VolumetricMicroscopySeries", + "MicroscopyStaticImage", + "PlanarMicroscopyStaticImage", + "VolumetricMicroscopyStaticImage", "MultiPlaneMicroscopyContainer", "MultiChannelMicroscopyContainer", "MicroscopyResponseSeries", diff --git a/src/pynwb/ndx_microscopy/testing/__init__.py b/src/pynwb/ndx_microscopy/testing/__init__.py index 99f8579..6f9732a 100644 --- a/src/pynwb/ndx_microscopy/testing/__init__.py +++ b/src/pynwb/ndx_microscopy/testing/__init__.py @@ -11,10 +11,12 @@ mock_MicroscopyResponseSeriesContainer, mock_PlanarImagingSpace, mock_PlanarMicroscopySeries, + mock_PlanarMicroscopyStaticImage, mock_MultiPlaneMicroscopyContainer, mock_MultiChannelMicroscopyContainer, mock_VolumetricImagingSpace, mock_VolumetricMicroscopySeries, + mock_VolumetricMicroscopyStaticImage, mock_IlluminationPattern, mock_LineScan, mock_PlaneAcquisition, @@ -33,9 +35,11 @@ "mock_SegmentationContainer", "mock_Segmentation", "mock_PlanarMicroscopySeries", + "mock_PlanarMicroscopyStaticImage", "mock_MultiPlaneMicroscopyContainer", "mock_MultiChannelMicroscopyContainer", "mock_VolumetricMicroscopySeries", + "mock_VolumetricMicroscopyStaticImage", "mock_MicroscopyResponseSeries", "mock_MicroscopyResponseSeriesContainer", "mock_IlluminationPattern", diff --git a/src/pynwb/ndx_microscopy/testing/_mock.py b/src/pynwb/ndx_microscopy/testing/_mock.py index 4050d5b..d6152eb 100644 --- a/src/pynwb/ndx_microscopy/testing/_mock.py +++ b/src/pynwb/ndx_microscopy/testing/_mock.py @@ -396,15 +396,40 @@ def mock_PlanarMicroscopySeries( return planar_microscopy_series +def mock_PlanarMicroscopyStaticImage( + *, + microscopy_rig: ndx_microscopy.MicroscopyRig, + planar_imaging_space: ndx_microscopy.PlanarImagingSpace, + microscopy_channel: ndx_microscopy.MicroscopyChannel, + name: Optional[str] = None, + description: str = "A mock instance of a PlanarMicroscopyStaticImage type to be used for rapid testing.", + data: Optional[np.ndarray] = None, +) -> ndx_microscopy.PlanarMicroscopyStaticImage: + image_name = name or name_generator("PlanarMicroscopyStaticImage") + image_data = data if data is not None else np.ones(shape=(5, 5)) + + planar_microscopy_static_image = ndx_microscopy.PlanarMicroscopyStaticImage( + name=image_name, + description=description, + microscopy_rig=microscopy_rig, + microscopy_channel=microscopy_channel, + planar_imaging_space=planar_imaging_space, + data=image_data, + ) + return planar_microscopy_static_image + + def mock_MultiPlaneMicroscopyContainer( *, - planar_microscopy_series: List[ndx_microscopy.PlanarMicroscopySeries], + planar_microscopy_series: Optional[List[ndx_microscopy.PlanarMicroscopySeries]] = None, + planar_microscopy_static_images: Optional[List[ndx_microscopy.PlanarMicroscopyStaticImage]] = None, name: Optional[str] = None, ) -> ndx_microscopy.MultiPlaneMicroscopyContainer: container_name = name or name_generator("MultiPlaneMicroscopyContainer") - multi_plane_microscopy_container = ndx_microscopy.MultiPlaneMicroscopyContainer( - name=container_name, planar_microscopy_series=planar_microscopy_series + name=container_name, + planar_microscopy_series=planar_microscopy_series, + planar_microscopy_static_images=planar_microscopy_static_images, ) return multi_plane_microscopy_container @@ -412,15 +437,17 @@ def mock_MultiPlaneMicroscopyContainer( def mock_MultiChannelMicroscopyContainer( *, - microscopy_series: List[ndx_microscopy.MicroscopySeries], + microscopy_series: Optional[List[ndx_microscopy.MicroscopySeries]] = None, + microscopy_static_images: Optional[List[ndx_microscopy.MicroscopyStaticImage]] = None, name: Optional[str] = None, ) -> ndx_microscopy.MultiChannelMicroscopyContainer: container_name = name or name_generator("MultiChannelMicroscopyContainer") multi_channel_microscopy_container = ndx_microscopy.MultiChannelMicroscopyContainer( - name=container_name, microscopy_series=microscopy_series + name=container_name, + microscopy_series=microscopy_series, + microscopy_static_images=microscopy_static_images, ) - return multi_channel_microscopy_container @@ -477,6 +504,29 @@ def mock_VolumetricMicroscopySeries( return volumetric_microscopy_series +def mock_VolumetricMicroscopyStaticImage( + *, + microscopy_rig: ndx_microscopy.MicroscopyRig, + volumetric_imaging_space: ndx_microscopy.VolumetricImagingSpace, + microscopy_channel: ndx_microscopy.MicroscopyChannel, + name: Optional[str] = None, + description: str = "A mock instance of a VolumetricMicroscopyStaticImage type to be used for rapid testing.", + data: Optional[np.ndarray] = None, +) -> ndx_microscopy.VolumetricMicroscopyStaticImage: + image_name = name or name_generator("VolumetricMicroscopyStaticImage") + image_data = data if data is not None else np.ones(shape=(5, 5, 5)) + + volumetric_microscopy_static_image = ndx_microscopy.VolumetricMicroscopyStaticImage( + name=image_name, + description=description, + microscopy_rig=microscopy_rig, + microscopy_channel=microscopy_channel, + volumetric_imaging_space=volumetric_imaging_space, + data=image_data, + ) + return volumetric_microscopy_static_image + + def mock_MicroscopyResponseSeries( *, rois: pynwb.core.DynamicTableRegion, diff --git a/src/pynwb/tests/test_constructors.py b/src/pynwb/tests/test_constructors.py index 8838e49..00f4e85 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.""" +from ndx_microscopy.testing._mock import mock_PlanarMicroscopyStaticImage, mock_VolumetricMicroscopyStaticImage import pytest from ndx_microscopy.testing import ( @@ -201,7 +202,25 @@ def test_constructor_planar_microscopy_series(): ) -def test_constructor_multi_plane_microscopy_container(): +def test_constructor_planar_microscopy_static_image(): + from ndx_ophys_devices.testing import mock_Indicator + + microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) + microscopy_rig = mock_MicroscopyRig() + planar_imaging_space = mock_PlanarImagingSpace() + + planar_microscopy_static_image = mock_PlanarMicroscopyStaticImage( + microscopy_rig=microscopy_rig, + microscopy_channel=microscopy_channel, + planar_imaging_space=planar_imaging_space, + ) + assert ( + planar_microscopy_static_image.description + == "A mock instance of a PlanarMicroscopyStaticImage type to be used for rapid testing." + ) + + +def test_constructor_multi_plane_microscopy_container_with_series(): from ndx_ophys_devices.testing import mock_Indicator microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) @@ -220,7 +239,26 @@ def test_constructor_multi_plane_microscopy_container(): assert multi_plane_microscopy_container.name == "MultiPlaneMicroscopyContainer" -def test_constructor_multi_channel_microscopy_container(): +def test_constructor_multi_plane_microscopy_container_with_static_images(): + from ndx_ophys_devices.testing import mock_Indicator + + microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) + microscopy_rig = mock_MicroscopyRig() + planar_imaging_space = mock_PlanarImagingSpace() + + planar_microscopy_static_image = mock_PlanarMicroscopyStaticImage( + microscopy_rig=microscopy_rig, + microscopy_channel=microscopy_channel, + planar_imaging_space=planar_imaging_space, + ) + + multi_plane_microscopy_container = mock_MultiPlaneMicroscopyContainer( + planar_microscopy_static_images=[planar_microscopy_static_image] + ) + assert multi_plane_microscopy_container.name == "MultiPlaneMicroscopyContainer" + + +def test_constructor_multi_channel_microscopy_container_with_series(): from ndx_ophys_devices.testing import mock_Indicator microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) @@ -238,6 +276,24 @@ def test_constructor_multi_channel_microscopy_container(): assert multi_channel_microscopy_container.name == "MultiChannelMicroscopyContainer" +def test_constructor_multi_channel_microscopy_container_with_static_images(): + from ndx_ophys_devices.testing import mock_Indicator + + microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) + microscopy_rig = mock_MicroscopyRig() + planar_imaging_space = mock_PlanarImagingSpace() + planar_microscopy_static_image = mock_PlanarMicroscopyStaticImage( + microscopy_rig=microscopy_rig, + microscopy_channel=microscopy_channel, + planar_imaging_space=planar_imaging_space, + ) + + multi_channel_microscopy_container = mock_MultiChannelMicroscopyContainer( + microscopy_static_images=[planar_microscopy_static_image] + ) + assert multi_channel_microscopy_container.name == "MultiChannelMicroscopyContainer" + + def test_constructor_volumetric_microscopy_series(): from ndx_ophys_devices.testing import mock_Indicator @@ -256,6 +312,24 @@ def test_constructor_volumetric_microscopy_series(): ) +def test_constructor_volumetric_microscopy_static_image(): + from ndx_ophys_devices.testing import mock_Indicator + + microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) + microscopy_rig = mock_MicroscopyRig() + volumetric_imaging_space = mock_VolumetricImagingSpace() + + volumetric_microscopy_static_image = mock_VolumetricMicroscopyStaticImage( + microscopy_rig=microscopy_rig, + microscopy_channel=microscopy_channel, + volumetric_imaging_space=volumetric_imaging_space, + ) + assert ( + volumetric_microscopy_static_image.description + == "A mock instance of a VolumetricMicroscopyStaticImage type to be used for rapid testing." + ) + + def test_constructor_microscopy_response_series(): number_of_rois = 10 planar_imaging_space = mock_PlanarImagingSpace() diff --git a/src/pynwb/tests/test_roundtrip.py b/src/pynwb/tests/test_roundtrip.py index 2a122fe..751666e 100644 --- a/src/pynwb/tests/test_roundtrip.py +++ b/src/pynwb/tests/test_roundtrip.py @@ -2,6 +2,7 @@ from datetime import datetime +from ndx_microscopy.testing._mock import mock_PlanarMicroscopyStaticImage, mock_VolumetricMicroscopyStaticImage from pytz import UTC import pytest from pynwb.testing import TestCase as pynwb_TestCase @@ -41,6 +42,182 @@ from ndx_microscopy import MicroscopyExperimentMetadata, MicroscopyResponseSeriesContainer +class TestPlanarMicroscopyStaticImageSimpleRoundtrip(pynwb_TestCase): + """Simple roundtrip test for PlanarMicroscopyStaticImage.""" + + def setUp(self): + self.nwbfile_path = "test_planar_microscopy_static_image_roundtrip.nwb" + + def tearDown(self): + pynwb.testing.remove_test_file(self.nwbfile_path) + + def test_roundtrip(self): + nwbfile = mock_NWBFile(session_start_time=datetime(2000, 1, 1, tzinfo=UTC)) + + microscope_model = mock_MicroscopeModel(name="MicroscopeModel") + nwbfile.add_device_model(device_models=microscope_model) + microscope = mock_Microscope(name="Microscope", model=microscope_model) + nwbfile.add_device(devices=microscope) + + excitation_source_model = mock_ExcitationSourceModel(name="ExcitationSourceModel") + nwbfile.add_device_model(device_models=excitation_source_model) + excitation_source = mock_ExcitationSource(model=excitation_source_model) + nwbfile.add_device(devices=excitation_source) + + excitation_filter_model = mock_OpticalFilterModel(name="OpticalFilterModel") + nwbfile.add_device_model(device_models=excitation_filter_model) + excitation_filter = mock_OpticalFilter(model=excitation_filter_model) + nwbfile.add_device(devices=excitation_filter) + + dichroic_mirror_model = mock_DichroicMirrorModel(name="DichroicMirrorModel") + nwbfile.add_device_model(device_models=dichroic_mirror_model) + dichroic_mirror = mock_DichroicMirror(model=dichroic_mirror_model) + nwbfile.add_device(devices=dichroic_mirror) + + photodetector_model = mock_PhotodetectorModel(name="PhotodetectorModel") + nwbfile.add_device_model(device_models=photodetector_model) + photodetector = mock_Photodetector(model=photodetector_model) + nwbfile.add_device(devices=photodetector) + + emission_filter_model = mock_OpticalFilterModel(name="EmissionFilterModel") + nwbfile.add_device_model(device_models=emission_filter_model) + emission_filter = mock_OpticalFilter(model=emission_filter_model) + nwbfile.add_device(devices=emission_filter) + + microscopy_rig = mock_MicroscopyRig( + name="MicroscopyRig", + microscope=microscope, + excitation_source=excitation_source, + excitation_filter=excitation_filter, + emission_filter=emission_filter, + photodetector=photodetector, + dichroic_mirror=dichroic_mirror, + ) + viral_vector = mock_ViralVector(name="ViralVector1") + viral_vector_injection = mock_ViralVectorInjection( + name="ViralVectorInjection1", + viral_vector=viral_vector, + ) + indicator = mock_Indicator(name="Indicator1", viral_vector_injection=viral_vector_injection) + + microscopy_experiment_metadata = MicroscopyExperimentMetadata( + viral_vectors=[viral_vector], + viral_vector_injections=[viral_vector_injection], + indicators=[indicator], + microscopy_rigs=[microscopy_rig], + ) + nwbfile.add_lab_meta_data(microscopy_experiment_metadata) + + planar_imaging_space = mock_PlanarImagingSpace(name="PlanarImagingSpace") + + planar_microscopy_static_image = mock_PlanarMicroscopyStaticImage( + name="PlanarMicroscopyStaticImage", + microscopy_rig=microscopy_rig, + microscopy_channel=mock_MicroscopyChannel(name="MicroscopyChannel", indicator=indicator), + planar_imaging_space=planar_imaging_space, + ) + + nwbfile.add_acquisition(planar_microscopy_static_image) + + 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( + planar_microscopy_static_image, read_nwbfile.acquisition["PlanarMicroscopyStaticImage"] + ) + + +class TestVolumetricMicroscopyStaticImageSimpleRoundtrip(pynwb_TestCase): + """Simple roundtrip test for VolumetricMicroscopyStaticImage.""" + + def setUp(self): + self.nwbfile_path = "test_volumetric_microscopy_static_image_roundtrip.nwb" + + def tearDown(self): + pynwb.testing.remove_test_file(self.nwbfile_path) + + def test_roundtrip(self): + nwbfile = mock_NWBFile(session_start_time=datetime(2000, 1, 1, tzinfo=UTC)) + + microscope_model = mock_MicroscopeModel(name="MicroscopeModel") + nwbfile.add_device_model(device_models=microscope_model) + microscope = mock_Microscope(name="Microscope", model=microscope_model) + nwbfile.add_device(devices=microscope) + + excitation_source_model = mock_ExcitationSourceModel(name="ExcitationSourceModel") + nwbfile.add_device_model(device_models=excitation_source_model) + excitation_source = mock_ExcitationSource(model=excitation_source_model) + nwbfile.add_device(devices=excitation_source) + + excitation_filter_model = mock_OpticalFilterModel(name="OpticalFilterModel") + nwbfile.add_device_model(device_models=excitation_filter_model) + excitation_filter = mock_OpticalFilter(model=excitation_filter_model) + nwbfile.add_device(devices=excitation_filter) + + dichroic_mirror_model = mock_DichroicMirrorModel(name="DichroicMirrorModel") + nwbfile.add_device_model(device_models=dichroic_mirror_model) + dichroic_mirror = mock_DichroicMirror(model=dichroic_mirror_model) + nwbfile.add_device(devices=dichroic_mirror) + + photodetector_model = mock_PhotodetectorModel(name="PhotodetectorModel") + nwbfile.add_device_model(device_models=photodetector_model) + photodetector = mock_Photodetector(model=photodetector_model) + nwbfile.add_device(devices=photodetector) + + emission_filter_model = mock_OpticalFilterModel(name="EmissionFilterModel") + nwbfile.add_device_model(device_models=emission_filter_model) + emission_filter = mock_OpticalFilter(model=emission_filter_model) + nwbfile.add_device(devices=emission_filter) + + microscopy_rig = mock_MicroscopyRig( + name="MicroscopyRig", + microscope=microscope, + excitation_source=excitation_source, + excitation_filter=excitation_filter, + emission_filter=emission_filter, + photodetector=photodetector, + dichroic_mirror=dichroic_mirror, + ) + + viral_vector = mock_ViralVector(name="ViralVector1") + viral_vector_injection = mock_ViralVectorInjection( + name="ViralVectorInjection1", + viral_vector=viral_vector, + ) + indicator = mock_Indicator(name="Indicator1", viral_vector_injection=viral_vector_injection) + + microscopy_experiment_metadata = MicroscopyExperimentMetadata( + viral_vectors=[viral_vector], + viral_vector_injections=[viral_vector_injection], + indicators=[indicator], + microscopy_rigs=[microscopy_rig], + ) + nwbfile.add_lab_meta_data(microscopy_experiment_metadata) + + volumetric_imaging_space = mock_VolumetricImagingSpace(name="VolumetricImagingSpace") + volumetric_microscopy_static_image = mock_VolumetricMicroscopyStaticImage( + name="VolumetricMicroscopyStaticImage", + volumetric_imaging_space=volumetric_imaging_space, + microscopy_rig=microscopy_rig, + microscopy_channel=mock_MicroscopyChannel(name="MicroscopyChannel", indicator=indicator), + ) + + nwbfile.add_acquisition(volumetric_microscopy_static_image) + + 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( + volumetric_microscopy_static_image, read_nwbfile.acquisition["VolumetricMicroscopyStaticImage"] + ) + + class TestPlanarMicroscopySeriesSimpleRoundtrip(pynwb_TestCase): """Simple roundtrip test for PlanarMicroscopySeries.""" From 73a4097efbc1221f506ca2aa2dafe3a73f968aca Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Wed, 3 Dec 2025 12:31:16 +0100 Subject: [PATCH 11/16] Add MicroscopyStaticImageContainer and related mock functions --- spec/ndx-microscopy.extensions.yaml | 33 ++++++++++++-------- spec/ndx-microscopy.namespace.yaml | 3 +- src/pynwb/ndx_microscopy/__init__.py | 2 ++ src/pynwb/ndx_microscopy/testing/_mock.py | 18 ++++++++--- src/pynwb/tests/test_constructors.py | 37 +++++++---------------- 5 files changed, 50 insertions(+), 43 deletions(-) diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml index 0260123..e42a30c 100644 --- a/spec/ndx-microscopy.extensions.yaml +++ b/spec/ndx-microscopy.extensions.yaml @@ -245,10 +245,17 @@ groups: quantity: "?" - neurodata_type_def: MicroscopyStaticImage - neurodata_type_inc: Image --> bisogna usare per forza container + neurodata_type_inc: NWBDataInterface doc: Abstract class to contain static imaging data acquired from an optical channel in a microscope while a light source illuminates the imaging space. + attributes: + - name: name + dtype: text + doc: Name of the static image. + - name: description + dtype: text + doc: Description of the static image. links: - name: microscopy_rig doc: Link to a MicroscopyRig object containing metadata about the microscopy rig used to acquire this imaging data. @@ -303,6 +310,15 @@ groups: VolumetricImagingSpace object containing metadata about the region of physical space this imaging data was recorded from. + - neurodata_type_def: MicroscopyStaticImageContainer + neurodata_type_inc: NWBDataInterface + default_name: MicroscopyStaticImageContainer + doc: A container of many MicroscopyStaticImage objects. + groups: + - neurodata_type_inc: MicroscopyStaticImage + doc: MicroscopyStaticImage object containing imaging data for a single channel scan. + quantity: "+" + - neurodata_type_def: MicroscopySeries neurodata_type_inc: TimeSeries doc: @@ -371,29 +387,22 @@ groups: default_name: MultiPlaneMicroscopyContainer doc: Imaging data acquired over several depths, regularly or irregularly spaced; for instance, when using an - electrically tunable lens. Each depth scan is stored in a separate PlanarMicroscopySeries or PlanarMicroscopyStaticImage object. + electrically tunable lens. Each depth scan is stored in a separate PlanarMicroscopySeries object. groups: - neurodata_type_inc: PlanarMicroscopySeries doc: PlanarMicroscopySeries object(s) containing imaging data for a single depth scan. - quantity: "*" - - neurodata_type_inc: PlanarMicroscopyStaticImage - doc: PlanarMicroscopyStaticImage object(s) containing imaging data for a single depth scan. - quantity: "*" + quantity: "+" - neurodata_type_def: MultiChannelMicroscopyContainer neurodata_type_inc: NWBDataInterface default_name: MultiChannelMicroscopyContainer doc: Imaging data acquired over several channels; for instance, when using multiple excitation wavelengths - or multiple indicators. Each channel is stored in a separate PlanarMicroscopySeries, VolumetricMicroscopySeries, - PlanarMicroscopyStaticImage or VolumetricMicroscopyStaticImage. + or multiple indicators. Each channel is stored in a separate PlanarMicroscopySeries or VolumetricMicroscopySeries. groups: - neurodata_type_inc: MicroscopySeries doc: MicroscopySeries object containing imaging data for a single channel scan. - quantity: "*" - - neurodata_type_inc: MicroscopyStaticImage - doc: MicroscopyStaticImage object containing imaging data for a single channel scan. - quantity: "*" + quantity: "+" - neurodata_type_def: SegmentationContainer neurodata_type_inc: NWBDataInterface diff --git a/spec/ndx-microscopy.namespace.yaml b/spec/ndx-microscopy.namespace.yaml index 01c7b9a..750f6a7 100644 --- a/spec/ndx-microscopy.namespace.yaml +++ b/spec/ndx-microscopy.namespace.yaml @@ -20,11 +20,12 @@ namespaces: - VectorData - VectorIndex - DynamicTableRegion - - ImageSeries - namespace: ndx-ophys-devices neurodata_types: - ExcitationSource - Indicator + - ViralVectorInjection + - ViralVector - OpticalFilter - Photodetector - DichroicMirror diff --git a/src/pynwb/ndx_microscopy/__init__.py b/src/pynwb/ndx_microscopy/__init__.py index e6bc951..1f0e714 100644 --- a/src/pynwb/ndx_microscopy/__init__.py +++ b/src/pynwb/ndx_microscopy/__init__.py @@ -58,6 +58,7 @@ MicroscopyStaticImage = get_class("MicroscopyStaticImage", extension_name) PlanarMicroscopyStaticImage = get_class("PlanarMicroscopyStaticImage", extension_name) VolumetricMicroscopyStaticImage = get_class("VolumetricMicroscopyStaticImage", extension_name) +MicroscopyStaticImageContainer = get_class("MicroscopyStaticImageContainer", extension_name) MicroscopySeries = get_class("MicroscopySeries", extension_name) PlanarMicroscopySeries = get_class("PlanarMicroscopySeries", extension_name) VolumetricMicroscopySeries = get_class("VolumetricMicroscopySeries", extension_name) @@ -97,6 +98,7 @@ "MicroscopyStaticImage", "PlanarMicroscopyStaticImage", "VolumetricMicroscopyStaticImage", + "MicroscopyStaticImageContainer", "MultiPlaneMicroscopyContainer", "MultiChannelMicroscopyContainer", "MicroscopyResponseSeries", diff --git a/src/pynwb/ndx_microscopy/testing/_mock.py b/src/pynwb/ndx_microscopy/testing/_mock.py index d6152eb..90f03db 100644 --- a/src/pynwb/ndx_microscopy/testing/_mock.py +++ b/src/pynwb/ndx_microscopy/testing/_mock.py @@ -419,17 +419,29 @@ def mock_PlanarMicroscopyStaticImage( return planar_microscopy_static_image +def mock_MicroscopyStaticImageContainer( + *, + microscopy_static_images: Optional[List[ndx_microscopy.MicroscopyStaticImage]] = None, + name: Optional[str] = None, +) -> ndx_microscopy.MicroscopyStaticImageContainer: + container_name = name or name_generator("MicroscopyStaticImageContainer") + + microscopy_static_image_container = ndx_microscopy.MicroscopyStaticImageContainer( + name=container_name, + microscopy_static_images=microscopy_static_images, + ) + return microscopy_static_image_container + + def mock_MultiPlaneMicroscopyContainer( *, planar_microscopy_series: Optional[List[ndx_microscopy.PlanarMicroscopySeries]] = None, - planar_microscopy_static_images: Optional[List[ndx_microscopy.PlanarMicroscopyStaticImage]] = None, name: Optional[str] = None, ) -> ndx_microscopy.MultiPlaneMicroscopyContainer: container_name = name or name_generator("MultiPlaneMicroscopyContainer") multi_plane_microscopy_container = ndx_microscopy.MultiPlaneMicroscopyContainer( name=container_name, planar_microscopy_series=planar_microscopy_series, - planar_microscopy_static_images=planar_microscopy_static_images, ) return multi_plane_microscopy_container @@ -438,7 +450,6 @@ def mock_MultiPlaneMicroscopyContainer( def mock_MultiChannelMicroscopyContainer( *, microscopy_series: Optional[List[ndx_microscopy.MicroscopySeries]] = None, - microscopy_static_images: Optional[List[ndx_microscopy.MicroscopyStaticImage]] = None, name: Optional[str] = None, ) -> ndx_microscopy.MultiChannelMicroscopyContainer: container_name = name or name_generator("MultiChannelMicroscopyContainer") @@ -446,7 +457,6 @@ def mock_MultiChannelMicroscopyContainer( multi_channel_microscopy_container = ndx_microscopy.MultiChannelMicroscopyContainer( name=container_name, microscopy_series=microscopy_series, - microscopy_static_images=microscopy_static_images, ) return multi_channel_microscopy_container diff --git a/src/pynwb/tests/test_constructors.py b/src/pynwb/tests/test_constructors.py index 00f4e85..47dd38c 100644 --- a/src/pynwb/tests/test_constructors.py +++ b/src/pynwb/tests/test_constructors.py @@ -1,6 +1,10 @@ """Test in-memory Python API constructors for the ndx-microscopy extension.""" -from ndx_microscopy.testing._mock import mock_PlanarMicroscopyStaticImage, mock_VolumetricMicroscopyStaticImage +from ndx_microscopy.testing._mock import ( + mock_MicroscopyStaticImageContainer, + mock_PlanarMicroscopyStaticImage, + mock_VolumetricMicroscopyStaticImage, +) import pytest from ndx_microscopy.testing import ( @@ -73,7 +77,7 @@ def test_constructor_microscopy_experiment_metadata(): indicator = mock_Indicator(name="Indicator1", viral_vector_injection=viral_vector_injection) microscopy_rig = mock_MicroscopyRig() - microscopy_experiment_metadata = MicroscopyExperimentMetadata( + _ = MicroscopyExperimentMetadata( viral_vectors=[viral_vector], viral_vector_injections=[viral_vector_injection], indicators=[indicator], @@ -220,7 +224,7 @@ def test_constructor_planar_microscopy_static_image(): ) -def test_constructor_multi_plane_microscopy_container_with_series(): +def test_constructor_multi_plane_microscopy_container(): from ndx_ophys_devices.testing import mock_Indicator microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) @@ -239,26 +243,7 @@ def test_constructor_multi_plane_microscopy_container_with_series(): assert multi_plane_microscopy_container.name == "MultiPlaneMicroscopyContainer" -def test_constructor_multi_plane_microscopy_container_with_static_images(): - from ndx_ophys_devices.testing import mock_Indicator - - microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) - microscopy_rig = mock_MicroscopyRig() - planar_imaging_space = mock_PlanarImagingSpace() - - planar_microscopy_static_image = mock_PlanarMicroscopyStaticImage( - microscopy_rig=microscopy_rig, - microscopy_channel=microscopy_channel, - planar_imaging_space=planar_imaging_space, - ) - - multi_plane_microscopy_container = mock_MultiPlaneMicroscopyContainer( - planar_microscopy_static_images=[planar_microscopy_static_image] - ) - assert multi_plane_microscopy_container.name == "MultiPlaneMicroscopyContainer" - - -def test_constructor_multi_channel_microscopy_container_with_series(): +def test_constructor_multi_channel_microscopy_container(): from ndx_ophys_devices.testing import mock_Indicator microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) @@ -276,7 +261,7 @@ def test_constructor_multi_channel_microscopy_container_with_series(): assert multi_channel_microscopy_container.name == "MultiChannelMicroscopyContainer" -def test_constructor_multi_channel_microscopy_container_with_static_images(): +def test_constructor_microscopy_static_images_container(): from ndx_ophys_devices.testing import mock_Indicator microscopy_channel = mock_MicroscopyChannel(indicator=mock_Indicator(name="Indicator1")) @@ -288,10 +273,10 @@ def test_constructor_multi_channel_microscopy_container_with_static_images(): planar_imaging_space=planar_imaging_space, ) - multi_channel_microscopy_container = mock_MultiChannelMicroscopyContainer( + microscopy_static_image_container = mock_MicroscopyStaticImageContainer( microscopy_static_images=[planar_microscopy_static_image] ) - assert multi_channel_microscopy_container.name == "MultiChannelMicroscopyContainer" + assert microscopy_static_image_container.name == "MicroscopyStaticImageContainer" def test_constructor_volumetric_microscopy_series(): From 2b8cb45e10f0083e0988cdec60fece31178ee12f Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Wed, 3 Dec 2025 12:44:16 +0100 Subject: [PATCH 12/16] update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9977a8f..9055180 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ - Updated `ndx-ophys-devices` dependency from v0.2.0 to v0.4.0 ## Features +- Added static image support for microscopy experiments: + - `MicroscopyStaticImage`: Base class for static images + - `PlanarMicroscopyStaticImage`: For 2D static images + - `VolumetricMicroscopyStaticImage`: For 3D static images + - `MicroscopyStaticImageContainer`: Container for organizing static images - Added `MicroscopyExperimentMetadata` (extends `LabMetaData`) as a centralized container for experiment metadata, including: - `MicroscopyRig` objects - `ViralVector` objects (from ndx-ophys-devices) From 102ef08b2dab1c33052b6211dce9f9f5a9a732fa Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Wed, 3 Dec 2025 12:59:38 +0100 Subject: [PATCH 13/16] remove MicroscopyStaticImageContainer --- CHANGELOG.md | 4 ++++ spec/ndx-microscopy.extensions.yaml | 10 ++++++++-- spec/ndx-microscopy.namespace.yaml | 3 ++- src/pynwb/ndx_microscopy/testing/_mock.py | 3 +-- src/pynwb/tests/test_constructors.py | 5 +++-- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9977a8f..6510398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - Updated `ndx-ophys-devices` dependency from v0.2.0 to v0.4.0 ## Features +- Added static image support for microscopy experiments: + - `MicroscopyStaticImage`: Base class for static images + - `PlanarMicroscopyStaticImage`: For 2D static images + - `VolumetricMicroscopyStaticImage`: For 3D static images - Added `MicroscopyExperimentMetadata` (extends `LabMetaData`) as a centralized container for experiment metadata, including: - `MicroscopyRig` objects - `ViralVector` objects (from ndx-ophys-devices) diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml index 0260123..13c1e48 100644 --- a/spec/ndx-microscopy.extensions.yaml +++ b/spec/ndx-microscopy.extensions.yaml @@ -245,10 +245,17 @@ groups: quantity: "?" - neurodata_type_def: MicroscopyStaticImage - neurodata_type_inc: Image --> bisogna usare per forza container + neurodata_type_inc: NWBDataInterface doc: Abstract class to contain static imaging data acquired from an optical channel in a microscope while a light source illuminates the imaging space. + attributes: + - name: name + dtype: text + doc: Name of the static image. + - name: description + dtype: text + doc: Description of the static image. links: - name: microscopy_rig doc: Link to a MicroscopyRig object containing metadata about the microscopy rig used to acquire this imaging data. @@ -564,4 +571,3 @@ groups: - neurodata_type_inc: MicroscopyResponseSeries doc: MicroscopyResponseSeries object(s) containing fluorescence data for a ROI. quantity: "+" -#TODO add static images diff --git a/spec/ndx-microscopy.namespace.yaml b/spec/ndx-microscopy.namespace.yaml index 01c7b9a..750f6a7 100644 --- a/spec/ndx-microscopy.namespace.yaml +++ b/spec/ndx-microscopy.namespace.yaml @@ -20,11 +20,12 @@ namespaces: - VectorData - VectorIndex - DynamicTableRegion - - ImageSeries - namespace: ndx-ophys-devices neurodata_types: - ExcitationSource - Indicator + - ViralVectorInjection + - ViralVector - OpticalFilter - Photodetector - DichroicMirror diff --git a/src/pynwb/ndx_microscopy/testing/_mock.py b/src/pynwb/ndx_microscopy/testing/_mock.py index d6152eb..fe42c13 100644 --- a/src/pynwb/ndx_microscopy/testing/_mock.py +++ b/src/pynwb/ndx_microscopy/testing/_mock.py @@ -3,8 +3,7 @@ import ndx_ophys_devices import numpy as np -import pynwb.base -from ndx_ophys_devices import ExcitationSource, OpticalFilter, Photodetector, DichroicMirror, Indicator +from ndx_ophys_devices import ExcitationSource, OpticalFilter, Photodetector, DichroicMirror from ndx_ophys_devices.testing import ( mock_ExcitationSource, diff --git a/src/pynwb/tests/test_constructors.py b/src/pynwb/tests/test_constructors.py index 00f4e85..5fe353c 100644 --- a/src/pynwb/tests/test_constructors.py +++ b/src/pynwb/tests/test_constructors.py @@ -1,6 +1,5 @@ """Test in-memory Python API constructors for the ndx-microscopy extension.""" -from ndx_microscopy.testing._mock import mock_PlanarMicroscopyStaticImage, mock_VolumetricMicroscopyStaticImage import pytest from ndx_microscopy.testing import ( @@ -14,6 +13,8 @@ mock_SegmentationContainer, mock_PlanarImagingSpace, mock_PlanarMicroscopySeries, + mock_PlanarMicroscopyStaticImage, + mock_VolumetricMicroscopyStaticImage, mock_MultiPlaneMicroscopyContainer, mock_MultiChannelMicroscopyContainer, mock_VolumetricImagingSpace, @@ -73,7 +74,7 @@ def test_constructor_microscopy_experiment_metadata(): indicator = mock_Indicator(name="Indicator1", viral_vector_injection=viral_vector_injection) microscopy_rig = mock_MicroscopyRig() - microscopy_experiment_metadata = MicroscopyExperimentMetadata( + _ = MicroscopyExperimentMetadata( viral_vectors=[viral_vector], viral_vector_injections=[viral_vector_injection], indicators=[indicator], From d07043e6f19d5ab566994d531670e8d41097b57e Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Wed, 3 Dec 2025 12:59:54 +0100 Subject: [PATCH 14/16] update tests --- src/pynwb/ndx_microscopy/testing/_mock.py | 1 + src/pynwb/tests/test_constructors.py | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pynwb/ndx_microscopy/testing/_mock.py b/src/pynwb/ndx_microscopy/testing/_mock.py index fe42c13..f2bf656 100644 --- a/src/pynwb/ndx_microscopy/testing/_mock.py +++ b/src/pynwb/ndx_microscopy/testing/_mock.py @@ -1,6 +1,7 @@ import warnings from typing import List, Optional, Tuple +import pynwb import ndx_ophys_devices import numpy as np from ndx_ophys_devices import ExcitationSource, OpticalFilter, Photodetector, DichroicMirror diff --git a/src/pynwb/tests/test_constructors.py b/src/pynwb/tests/test_constructors.py index 5fe353c..cce698a 100644 --- a/src/pynwb/tests/test_constructors.py +++ b/src/pynwb/tests/test_constructors.py @@ -235,9 +235,9 @@ def test_constructor_multi_plane_microscopy_container_with_series(): ) multi_plane_microscopy_container = mock_MultiPlaneMicroscopyContainer( - planar_microscopy_series=[planar_microscopy_series] + name="MultiPlaneMicroscopySeriesContainer", planar_microscopy_series=[planar_microscopy_series] ) - assert multi_plane_microscopy_container.name == "MultiPlaneMicroscopyContainer" + assert multi_plane_microscopy_container.name == "MultiPlaneMicroscopySeriesContainer" def test_constructor_multi_plane_microscopy_container_with_static_images(): @@ -254,9 +254,10 @@ def test_constructor_multi_plane_microscopy_container_with_static_images(): ) multi_plane_microscopy_container = mock_MultiPlaneMicroscopyContainer( - planar_microscopy_static_images=[planar_microscopy_static_image] + name="MultiPlaneMicroscopyStaticImageContainer", + planar_microscopy_static_images=[planar_microscopy_static_image], ) - assert multi_plane_microscopy_container.name == "MultiPlaneMicroscopyContainer" + assert multi_plane_microscopy_container.name == "MultiPlaneMicroscopyStaticImageContainer" def test_constructor_multi_channel_microscopy_container_with_series(): @@ -272,9 +273,9 @@ def test_constructor_multi_channel_microscopy_container_with_series(): ) multi_channel_microscopy_container = mock_MultiChannelMicroscopyContainer( - microscopy_series=[planar_microscopy_series] + name="MultiChannelPlanarMicroscopySeriesContainer", microscopy_series=[planar_microscopy_series] ) - assert multi_channel_microscopy_container.name == "MultiChannelMicroscopyContainer" + assert multi_channel_microscopy_container.name == "MultiChannelPlanarMicroscopySeriesContainer" def test_constructor_multi_channel_microscopy_container_with_static_images(): @@ -290,9 +291,10 @@ def test_constructor_multi_channel_microscopy_container_with_static_images(): ) multi_channel_microscopy_container = mock_MultiChannelMicroscopyContainer( - microscopy_static_images=[planar_microscopy_static_image] + name="MultiChannelPlanarMicroscopyStaticImageContainer", + microscopy_static_images=[planar_microscopy_static_image], ) - assert multi_channel_microscopy_container.name == "MultiChannelMicroscopyContainer" + assert multi_channel_microscopy_container.name == "MultiChannelPlanarMicroscopyStaticImageContainer" def test_constructor_volumetric_microscopy_series(): From 340bfe1c75a719e9df59113899d115cf5275acdb Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Wed, 3 Dec 2025 13:04:28 +0100 Subject: [PATCH 15/16] Remove MicroscopyStaticImageContainer definition and references --- spec/ndx-microscopy.extensions.yaml | 9 --------- src/pynwb/ndx_microscopy/__init__.py | 2 -- 2 files changed, 11 deletions(-) diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml index bc4b8d8..7eecd4f 100644 --- a/spec/ndx-microscopy.extensions.yaml +++ b/spec/ndx-microscopy.extensions.yaml @@ -310,15 +310,6 @@ groups: VolumetricImagingSpace object containing metadata about the region of physical space this imaging data was recorded from. - - neurodata_type_def: MicroscopyStaticImageContainer - neurodata_type_inc: NWBDataInterface - default_name: MicroscopyStaticImageContainer - doc: A container of many MicroscopyStaticImage objects. - groups: - - neurodata_type_inc: MicroscopyStaticImage - doc: MicroscopyStaticImage object containing imaging data for a single channel scan. - quantity: "+" - - neurodata_type_def: MicroscopySeries neurodata_type_inc: TimeSeries doc: diff --git a/src/pynwb/ndx_microscopy/__init__.py b/src/pynwb/ndx_microscopy/__init__.py index 1f0e714..e6bc951 100644 --- a/src/pynwb/ndx_microscopy/__init__.py +++ b/src/pynwb/ndx_microscopy/__init__.py @@ -58,7 +58,6 @@ MicroscopyStaticImage = get_class("MicroscopyStaticImage", extension_name) PlanarMicroscopyStaticImage = get_class("PlanarMicroscopyStaticImage", extension_name) VolumetricMicroscopyStaticImage = get_class("VolumetricMicroscopyStaticImage", extension_name) -MicroscopyStaticImageContainer = get_class("MicroscopyStaticImageContainer", extension_name) MicroscopySeries = get_class("MicroscopySeries", extension_name) PlanarMicroscopySeries = get_class("PlanarMicroscopySeries", extension_name) VolumetricMicroscopySeries = get_class("VolumetricMicroscopySeries", extension_name) @@ -98,7 +97,6 @@ "MicroscopyStaticImage", "PlanarMicroscopyStaticImage", "VolumetricMicroscopyStaticImage", - "MicroscopyStaticImageContainer", "MultiPlaneMicroscopyContainer", "MultiChannelMicroscopyContainer", "MicroscopyResponseSeries", From db2fd99a94252c44fd372d25d2525f176c13f3ca Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Wed, 3 Dec 2025 13:10:56 +0100 Subject: [PATCH 16/16] fixes --- spec/ndx-microscopy.extensions.yaml | 10 ++++++++-- src/pynwb/ndx_microscopy/testing/_mock.py | 18 ++++-------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/spec/ndx-microscopy.extensions.yaml b/spec/ndx-microscopy.extensions.yaml index 7eecd4f..dc28f2a 100644 --- a/spec/ndx-microscopy.extensions.yaml +++ b/spec/ndx-microscopy.extensions.yaml @@ -382,7 +382,10 @@ groups: groups: - neurodata_type_inc: PlanarMicroscopySeries doc: PlanarMicroscopySeries object(s) containing imaging data for a single depth scan. - quantity: "+" + quantity: "*" + - neurodata_type_inc: PlanarMicroscopyStaticImage + doc: PlanarMicroscopyStaticImage object(s) containing imaging data for a single depth scan. + quantity: "*" - neurodata_type_def: MultiChannelMicroscopyContainer neurodata_type_inc: NWBDataInterface @@ -393,7 +396,10 @@ groups: groups: - neurodata_type_inc: MicroscopySeries doc: MicroscopySeries object containing imaging data for a single channel scan. - quantity: "+" + quantity: "*" + - neurodata_type_inc: MicroscopyStaticImage + doc: MicroscopyStaticImage object containing imaging data for a single channel scan. + quantity: "*" - neurodata_type_def: SegmentationContainer neurodata_type_inc: NWBDataInterface diff --git a/src/pynwb/ndx_microscopy/testing/_mock.py b/src/pynwb/ndx_microscopy/testing/_mock.py index 5082e4b..a024dad 100644 --- a/src/pynwb/ndx_microscopy/testing/_mock.py +++ b/src/pynwb/ndx_microscopy/testing/_mock.py @@ -419,28 +419,16 @@ def mock_PlanarMicroscopyStaticImage( return planar_microscopy_static_image -def mock_MicroscopyStaticImageContainer( - *, - microscopy_static_images: Optional[List[ndx_microscopy.MicroscopyStaticImage]] = None, - name: Optional[str] = None, -) -> ndx_microscopy.MicroscopyStaticImageContainer: - container_name = name or name_generator("MicroscopyStaticImageContainer") - - microscopy_static_image_container = ndx_microscopy.MicroscopyStaticImageContainer( - name=container_name, - microscopy_static_images=microscopy_static_images, - ) - return microscopy_static_image_container - - def mock_MultiPlaneMicroscopyContainer( *, + planar_microscopy_static_images: Optional[List[ndx_microscopy.PlanarMicroscopyStaticImage]] = None, planar_microscopy_series: Optional[List[ndx_microscopy.PlanarMicroscopySeries]] = None, name: Optional[str] = None, ) -> ndx_microscopy.MultiPlaneMicroscopyContainer: container_name = name or name_generator("MultiPlaneMicroscopyContainer") multi_plane_microscopy_container = ndx_microscopy.MultiPlaneMicroscopyContainer( name=container_name, + planar_microscopy_static_images=planar_microscopy_static_images, planar_microscopy_series=planar_microscopy_series, ) @@ -449,6 +437,7 @@ def mock_MultiPlaneMicroscopyContainer( def mock_MultiChannelMicroscopyContainer( *, + microscopy_static_images: Optional[List[ndx_microscopy.MicroscopyStaticImage]] = None, microscopy_series: Optional[List[ndx_microscopy.MicroscopySeries]] = None, name: Optional[str] = None, ) -> ndx_microscopy.MultiChannelMicroscopyContainer: @@ -456,6 +445,7 @@ def mock_MultiChannelMicroscopyContainer( multi_channel_microscopy_container = ndx_microscopy.MultiChannelMicroscopyContainer( name=container_name, + microscopy_static_images=microscopy_static_images, microscopy_series=microscopy_series, ) return multi_channel_microscopy_container