From 3a1e325a10f46db25ef4f20117502dca845e59df Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Mon, 1 Dec 2025 16:47:07 +0100 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 3d41923f3640d4ceb93f64982a545ec69580c6d0 Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Tue, 2 Dec 2025 16:02:32 +0100 Subject: [PATCH 7/9] 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 8/9] 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 4c07337a427aba5e61faf441f47a12db322426fb Mon Sep 17 00:00:00 2001 From: Alessandra Trapani Date: Wed, 25 Mar 2026 11:22:44 +0100 Subject: [PATCH 9/9] not use a strict equality for ndx-ophys-devices dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3d0eb92..15ab4d2 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.4.0" + "ndx-ophys-devices<=0.5.0" ] [project.urls]