diff --git a/README.md b/README.md index 02d13d0..ded0824 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,211 @@ This is an NWB extension for storing metadata of devices used in optical experim --- This extension was created using [ndx-template](https://github.com/nwb-extensions/ndx-template). + +## Entity relationship diagrams + +#### Indicator and Effector + +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#ffffff', "primaryBorderColor': '#144E73', 'lineColor': '#D96F32'}}}%% +classDiagram + direction BT + class Indicator{ + <> + -------------------------------------- + attributes + -------------------------------------- + label : text + description : text, optional + manufacturer : text, optional + injection_brain_region : text, optional + injection_coordinates_in_mm : numeric, length 3, optional + } + class Effector{ + <> + -------------------------------------- + attributes + -------------------------------------- + label : text + description : text, optional + manufacturer : text, optional + injection_brain_region : text, optional + injection_coordinates_in_mm : numeric, length 3, optional + } +``` +#### Optical Filters + +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#ffffff', "primaryBorderColor': '#144E73', 'lineColor': '#D96F32'}}}%% +classDiagram + direction BT + class DeviceModel{ + <> + -------------------------------------- + attributes + -------------------------------------- + model : text, optional + } + class DichroicMirror{ + <> + -------------------------------------- + attributes + -------------------------------------- + cut_on_wavelength_in_nm : numeric, optional + cut_off_wavelength_in_nm : numeric, optional + reflection_band_in_nm : numeric, optional + transmission_band_in_nm : numeric, optional + angle_of_incidence_in_degrees : numeric, optional + } + class OpticalFilter{ + <> + -------------------------------------- + attributes + -------------------------------------- + filter_type : text, optional + } + class BandOpticalFilter{ + <> + -------------------------------------- + attributes + -------------------------------------- + center_wavelength_in_nm : numeric + bandwidth_in_nm : numeric + } + class EdgeOpticalFilter{ + <> + -------------------------------------- + attributes + -------------------------------------- + cut_wavelength_in_nm : numeric + slope_in_percent_cut_wavelength : numeric, optional + slope_starting_transmission_in_percent : numeric, optional + slope_ending_transmission_in_percent : numeric, optional + } + DichroicMirror *-- DeviceModel : extends + OpticalFilter *-- DeviceModel : extends + BandOpticalFilter *-- OpticalFilter : extends + EdgeOpticalFilter *-- OpticalFilter : extends +``` +#### Devices in the microscopy setup +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#ffffff', "primaryBorderColor': '#144E73', 'lineColor': '#D96F32'}}}%% +classDiagram + direction BT + class DeviceModel{ + <> + -------------------------------------- + attributes + -------------------------------------- + model : text, optional + } + class ExcitationSource{ + <> + -------------------------------------- + attributes + -------------------------------------- + illumination_type : text, optional + excitation_wavelength_in_nm : numeric, optional + power_in_W : numeric, optional + intensity_in_W_per_m2 : numeric, optional + exposure_time_in_s : numeric, optional + } + class PulsedExcitationSource{ + <> + -------------------------------------- + attributes + -------------------------------------- + peak_power_in_W : numeric, optional + peak_pulse_energy_in_J : numeric, optional + pulse_rate_in_Hz : numeric, optional + } + class Photodetector{ + <> + -------------------------------------- + attributes + -------------------------------------- + detector_type : text, optional + detected_wavelength_in_nm : numeric, optional + gain : numeric, optional + gain_unit : text, false + } + class ObjectiveLens{ + <> + -------------------------------------- + attributes + -------------------------------------- + numerical_aperture : numeric, optional + magnification : numeric, optional + } + + ExcitationSource *-- DeviceModel : extends + PulsedExcitationSource *-- ExcitationSource : extends + Photodetector *-- DeviceModel : extends + ObjectiveLens *-- DeviceModel : extends + OpticalFilter *-- DeviceModel : extends + BandOpticalFilter *-- OpticalFilter : extends + EdgeOpticalFilter *-- OpticalFilter : extends + DichroicMirror *-- DeviceModel : extends +``` +#### Devices in the fiber photometry setup +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#ffffff', "primaryBorderColor': '#144E73', 'lineColor': '#D96F32'}}}%% +classDiagram + direction BT + class DeviceModel{ + <> + -------------------------------------- + attributes + -------------------------------------- + model : text, optional + } + class OpticalFiber{ + <> + -------------------------------------- + attributes + -------------------------------------- + numerical_aperture : numeric, optional + core_diameter_in_um : numeric, optional + } + class ExcitationSource{ + <> + -------------------------------------- + attributes + -------------------------------------- + illumination_type : text, optional + excitation_wavelength_in_nm : numeric, optional + power_in_W : numeric, optional + intensity_in_W_per_m2 : numeric, optional + exposure_time_in_s : numeric, optional + } + class PulsedExcitationSource{ + <> + -------------------------------------- + attributes + -------------------------------------- + peak_power_in_W : numeric, optional + peak_pulse_energy_in_J : numeric, optional + pulse_rate_in_Hz : numeric, optional + } + class Photodetector{ + <> + -------------------------------------- + attributes + -------------------------------------- + detector_type : text, optional + detected_wavelength_in_nm : numeric, optional + gain : numeric, optional + } + ExcitationSource *-- DeviceModel : extends + PulsedExcitationSource *-- ExcitationSource : extends + Photodetector *-- DeviceModel : extends + OpticalFiber *-- DeviceModel : extends + DichroicMirror *-- DeviceModel : extends + OpticalFilter *-- DeviceModel : extends + BandOpticalFilter *-- OpticalFilter : extends + EdgeOpticalFilter *-- OpticalFilter : extends +``` + +## Contributing + +Add a comment on how to contribute to this extension \ No newline at end of file diff --git a/notebooks/example.ipynb b/notebooks/example.ipynb index 7c9d222..2a4bdd2 100644 --- a/notebooks/example.ipynb +++ b/notebooks/example.ipynb @@ -1,5 +1,157 @@ { "cells": [ + { + "cell_type": "code", + "execution_count": 4, + "id": "4e3c7e61", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import base64\n", + "from IPython.display import Image, display\n", + "\n", + "def mm(graph):\n", + " graphbytes = graph.encode(\"utf8\")\n", + " base64_bytes = base64.b64encode(graphbytes)\n", + " base64_string = base64_bytes.decode(\"ascii\")\n", + " display(Image(url=\"https://mermaid.ink/img/\" + base64_string))\n", + "\n", + "mm(\"\"\"\n", + "%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#ffffff', \"primaryBorderColor': '#144E73', 'lineColor': '#D96F32'}}}%%\n", + "classDiagram\n", + " direction BT\n", + " class DeviceModel{\n", + " <>\n", + " --------------------------------------\n", + " attributes\n", + " --------------------------------------\n", + " model : text, optional\n", + " }\n", + " class Indicator{\n", + " <>\n", + " --------------------------------------\n", + " attributes\n", + " --------------------------------------\n", + " label : text\n", + " injection_location : text, optional\n", + " injection_coordinates_in_mm : numeric, length 3, optional\n", + " }\n", + " class Effector{\n", + " <>\n", + " --------------------------------------\n", + " attributes\n", + " --------------------------------------\n", + " label : text\n", + " injection_location : text, optional\n", + " injection_coordinates_in_mm : numeric, length 3, optional\n", + " }\n", + " class OpticalFiber{\n", + " <>\n", + " --------------------------------------\n", + " attributes\n", + " --------------------------------------\n", + " numerical_aperture : numeric, optional\n", + " core_diameter_in_um : numeric, optional\n", + " }\n", + " class ExcitationSource{\n", + " <>\n", + " --------------------------------------\n", + " attributes\n", + " --------------------------------------\n", + " illumination_type : text, optional\n", + " excitation_wavelength_in_nm : numeric, optional\n", + " power_in_W : numeric, optional\n", + " intensity_in_W_per_m2 : numeric, optional\n", + " exposure_time_in_s : numeric, optional\n", + " }\n", + " class PulsedExcitationSource{\n", + " <>\n", + " --------------------------------------\n", + " attributes\n", + " --------------------------------------\n", + " peak_power_in_W : numeric, optional\n", + " peak_pulse_energy_in_J : numeric, optional\n", + " pulse_rate_in_Hz : numeric, optional\n", + " }\n", + " class Photodetector{\n", + " <>\n", + " --------------------------------------\n", + " attributes\n", + " --------------------------------------\n", + " detector_type : text, optional\n", + " detected_wavelength_in_nm : numeric, optional\n", + " gain : numeric, optional\n", + " }\n", + " class DichroicMirror{\n", + " <>\n", + " --------------------------------------\n", + " attributes\n", + " --------------------------------------\n", + " cut_on_wavelength_in_nm : numeric, optional\n", + " cut_off_wavelength_in_nm : numeric, optional\n", + " reflection_bandwidth_in_nm : numeric, optional\n", + " transmission_bandwidth_in_nm : numeric, optional\n", + " angle_of_incidence_in_degrees : numeric, optional\n", + " }\n", + " class OpticalFilter{\n", + " <>\n", + " --------------------------------------\n", + " attributes\n", + " --------------------------------------\n", + " filter_type : text, optional\n", + " }\n", + " class BandOpticalFilter{\n", + " <>\n", + " --------------------------------------\n", + " attributes\n", + " --------------------------------------\n", + " center_wavelength_in_nm : numeric\n", + " bandwidth_in_nm : numeric\n", + " }\n", + " class EdgeOpticalFilter{\n", + " <>\n", + " --------------------------------------\n", + " attributes\n", + " --------------------------------------\n", + " cut_wavelength_in_nm : numeric\n", + " slope_in_percent_cut_wavelength : numeric, optional\n", + " slope_starting_transmission_in_percent : numeric, optional\n", + " slope_ending_transmission_in_percent : numeric, optional\n", + " }\n", + " class ObjectiveLens{\n", + " <>\n", + " --------------------------------------\n", + " attributes\n", + " --------------------------------------\n", + " numerical_aperture : numeric, optional\n", + " magnification : numeric, optional\n", + " }\n", + "\n", + " OpticalFiber *-- DeviceModel : extends\n", + " ExcitationSource *-- DeviceModel : extends\n", + " PulsedExcitationSource *-- ExcitationSource : extends\n", + " Photodetector *-- DeviceModel : extends\n", + " DichroicMirror *-- DeviceModel : extends\n", + " ObjectiveLens *-- DeviceModel : extends\n", + " OpticalFilter *-- DeviceModel : extends\n", + " BandOpticalFilter *-- OpticalFilter : extends\n", + " EdgeOpticalFilter *-- OpticalFilter : extends\n", + "\"\"\")" + ] + }, { "cell_type": "markdown", "id": "174c5018-1c0a-4f55-899d-049bb87f63d5", @@ -17,22 +169,15 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", - "\n", "from pynwb import NWBHDF5IO, NWBFile\n", - "from pynwb.testing.mock.device import mock_Device\n", - "from pynwb.testing.mock.ecephys import mock_ElectrodeGroup, mock_ElectrodeTable\n", "from pynwb.testing.mock.file import mock_NWBFile\n", "\n", - "from ndx_ophys_devices import TetrodeSeries\n", + "#from ndx_ophys_devices import TetrodeSeries\n", "\n", "\n", "def set_up_nwbfile(nwbfile: NWBFile = None):\n", - " \"\"\"Create an NWBFile with a Device, ElectrodeGroup, and 10 electrodes in the ElectrodeTable.\"\"\"\n", + " \"\"\"Create an NWBFile with a .\"\"\"\n", " nwbfile = nwbfile or mock_NWBFile()\n", - " device = mock_Device(nwbfile=nwbfile)\n", - " electrode_group = mock_ElectrodeGroup(device=device, nwbfile=nwbfile)\n", - " _ = mock_ElectrodeTable(n_rows=10, group=electrode_group, nwbfile=nwbfile)\n", "\n", " return nwbfile" ] @@ -41,9 +186,7 @@ "cell_type": "markdown", "id": "32be75e4-8fe9-401d-a613-8080f357d5f0", "metadata": {}, - "source": [ - "Create an `NWBFile` object and a `TetrodeSeries` object and add the `TetrodeSeries` object to the `NWBFile`" - ] + "source": "Create an `NWBFile` object and a `...` object and add the `...` object to the `NWBFile`" }, { "cell_type": "code", @@ -54,43 +197,8 @@ "source": [ "nwbfile = set_up_nwbfile()\n", "\n", - "all_electrodes = nwbfile.create_electrode_table_region(\n", - " region=list(range(0, 10)),\n", - " description=\"all the electrodes\",\n", - ")\n", - "\n", - "data = np.random.rand(100, 10)\n", - "tetrode_series = TetrodeSeries(\n", - " name=\"TetrodeSeries\",\n", - " description=\"description\",\n", - " data=data,\n", - " rate=1000.0,\n", - " electrodes=all_electrodes,\n", - " trode_id=1,\n", - ")\n", "\n", - "nwbfile.add_acquisition(tetrode_series)" - ] - }, - { - "cell_type": "markdown", - "id": "077a8d86-9d03-40e3-b60a-c837ecb643a7", - "metadata": {}, - "source": [ - "Visualize the TetrodeSeries object with the `nwbwidgets` package using the custom widget defined in the extension. You\n", - "will need the latest version of `nwbwidgets` installed for this to work." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1363282d-1a32-447d-9ca3-fc2360785cc2", - "metadata": {}, - "outputs": [], - "source": [ - "from nwbwidgets import nwb2widget, load_extension_widgets_into_spec\n", - "load_extension_widgets_into_spec(\"ndx_my_namespace\")\n", - "nwb2widget(nwbfile)" + "# nwbfile.add_acquisition(tetrode_series)" ] }, { @@ -129,16 +237,8 @@ "source": [ "with NWBHDF5IO(\"test.nwb\", \"r\") as io:\n", " read_nwbfile = io.read()\n", - " print(read_nwbfile.acquisition[\"TetrodeSeries\"])" + " # print(read_nwbfile.acquisition[\"TetrodeSeries\"])" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b36569b2-09d1-4281-bf4c-fb602e53636c", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -157,7 +257,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/spec/ndx-ophys-devices.extensions.yaml b/spec/ndx-ophys-devices.extensions.yaml index fa9a0ff..1f8466e 100644 --- a/spec/ndx-ophys-devices.extensions.yaml +++ b/spec/ndx-ophys-devices.extensions.yaml @@ -1,8 +1,226 @@ groups: -- neurodata_type_def: TetrodeSeries - neurodata_type_inc: ElectricalSeries - doc: An extension of ElectricalSeries to include the tetrode ID for each time series. - attributes: - - name: trode_id - dtype: int32 - doc: The tetrode ID. + - neurodata_type_def: DeviceModel + neurodata_type_inc: Device + doc: Extends Device to hold model metadata. + attributes: + - name: model + dtype: text + doc: Model of the device. + required: false + - neurodata_type_def: Indicator + neurodata_type_inc: NWBContainer + doc: An NWBContainer to hold metadata on the Indicator. + attributes: + - name: label + dtype: text + doc: Indicator standard notation. + - name: description + dtype: text + doc: Indicator description. + required: false + - name: manufacturer + dtype: text + doc: Indicator manufacturer. + required: false + - name: injection_brain_region + dtype: text + doc: Injection brain region name. + required: false + - name: injection_coordinates_in_mm + dtype: float + shape: + - 3 + doc: + Indicator injection location in stereotactic coordinates (AP, ML, DV) mm + relative to Bregma. + required: false + - neurodata_type_def: OpticalFiber + neurodata_type_inc: DeviceModel + doc: Extends DeviceModel to hold metadata on the Optical Fiber. + attributes: + - name: numerical_aperture + dtype: float + doc: Numerical aperture, e.g., 0.39 NA. + required: false + - name: core_diameter_in_um + dtype: float + doc: Core diameter in micrometers. + required: false + - neurodata_type_def: ExcitationSource + neurodata_type_inc: DeviceModel + doc: Extends DeviceModel to hold metadata on the Excitation Source. + attributes: + - name: illumination_type + dtype: text + doc: + "Type of illumination. Suggested values: LED, Gas Laser (e.g., Argon, Krypton), + Solid-State Laser (e.g., Diode, DPSS)." + - name: excitation_wavelength_in_nm + dtype: float + doc: Excitation wavelength of the stimulation light (nanometers). + - name: power_in_W + dtype: float + doc: Incident power of stimulation device (in Watts). + required: false + - name: intensity_in_W_per_m2 + dtype: float + doc: Intensity of the excitation in W/m^2, if known. + required: false + - name: exposure_time_in_s + dtype: float + doc: Exposure time of the sample (in sec). + required: false + - neurodata_type_def: PulsedExcitationSource + neurodata_type_inc: ExcitationSource + doc: Extends ExcitationSource to hold metadata on the Pulsed Excitation Source. + attributes: + - name: peak_power_in_W + dtype: float + doc: Incident peak power of stimulation device (in Watts). + required: false + - name: peak_pulse_energy_in_J + dtype: float + doc: If device is pulsed light source, pulse energy (in Joules). + required: false + - name: pulse_rate_in_Hz + dtype: float + doc: If device is pulsed light source, pulse rate (in Hz) used for stimulation. + required: false + - neurodata_type_def: Photodetector + neurodata_type_inc: DeviceModel + doc: Extends DeviceModel to hold metadata on the Photodetector. + attributes: + - name: detector_type + dtype: text + doc: + "Technology used to detect the light. Suggested values: CCD, Intensified + CCD, PMT, Photodiode, CMOS, EBCCD, FTIR." + - name: detected_wavelength_in_nm + dtype: float + doc: Wavelength detected by photodetector. + - name: gain + dtype: float + doc: Gain on the photodetector. + required: false + - name: gain_unit + dtype: text + doc: Gain on the photodetector. + required: false + - neurodata_type_def: DichroicMirror + neurodata_type_inc: DeviceModel + doc: Extends DeviceModel to hold a Dichroic Mirror. + attributes: + - name: cut_on_wavelength_in_nm + dtype: float + doc: Wavelength at which the mirror starts to transmit light more than reflect. + required: false + - name: cut_off_wavelength_in_nm + dtype: float + doc: + Wavelength at which transmission shifts back to reflection,for mirrors with + complex transmission spectra. + required: false + - name: reflection_band_in_nm + dtype: float + shape: + - 2 + doc: + The range of wavelengths that are primarily reflected.The start and end wavelengths + needs to be specified. + required: false + - name: transmission_band_in_nm + dtype: float + shape: + - 2 + doc: + The range of wavelengths that are primarily transmitted.The start and end + wavelengths needs to be specified. + required: false + - name: angle_of_incidence_in_degrees + dtype: float + doc: Intended angle at which light strikes the mirror. + required: false + - neurodata_type_def: OpticalFilter + neurodata_type_inc: DeviceModel + doc: Extends DeviceModel to hold a Optical Filter. + attributes: + - name: filter_type + dtype: text + doc: Type of filter (e.g., 'Bandpass', 'Bandstop', 'Longpass', 'Shortpass'). + - neurodata_type_def: BandOpticalFilter + neurodata_type_inc: OpticalFilter + doc: Extends OpticalFilter to hold a Band Optical Filter (Bandpass or Bandstop). + attributes: + - name: center_wavelength_in_nm + dtype: float + doc: The midpoint of the band of wavelengths that the filter transmits or blocks. + - name: bandwidth_in_nm + dtype: float + doc: + The width of the wavelength range that the filter transmits or blocks (full + width at half maximum). + - neurodata_type_def: EdgeOpticalFilter + neurodata_type_inc: OpticalFilter + doc: Extends OpticalFilter to hold an Edge Optical Filter (Longpass or Shortpass). + attributes: + - name: cut_wavelength_in_nm + dtype: float + doc: " The wavelength at which the filter transmits half as much as its peak transmission." + - name: slope_in_percent_cut_wavelength + dtype: float + doc: + The steepness of the transition from high blocking to high transmission (or + vice versa). Specified as a percentage of the cut wavelength. + required: false + - name: slope_starting_transmission_in_percent + dtype: float + doc: + The percent transmission that defines the starting point for the slope (e.g. + 10%). + required: false + - name: slope_ending_transmission_in_percent + dtype: float + doc: + The percent transmission that defines the ending point for the slope (e.g. + 80%). + required: false + - neurodata_type_def: ObjectiveLens + neurodata_type_inc: DeviceModel + doc: Extends DeviceModel to hold a Objective lens. + attributes: + - name: numerical_aperture + dtype: float + doc: Numerical aperture, e.g., 0.39 NA. + required: false + - name: magnification + dtype: float + doc: The magnification of the lens as specified by the manufacturer, i.e. '60.0' is a 60X lens. + required: false + - neurodata_type_def: Effector + neurodata_type_inc: NWBContainer + doc: An NWBContainer to hold metadata on the Effector or Opsin. + attributes: + - name: label + dtype: text + doc: Effector standard notation. + - name: description + dtype: text + doc: Effector description. + required: false + - name: manufacturer + dtype: text + doc: Effector manufacturer. + required: false + - name: injection_brain_region + dtype: text + doc: Injection brain region name. + required: false + - name: injection_coordinates_in_mm + dtype: float + shape: + - 3 + doc: + Effector injection location in stereotactic coordinates (AP, ML, DV) mm + relative to Bregma. + required: false + diff --git a/spec/ndx-ophys-devices.namespace.yaml b/spec/ndx-ophys-devices.namespace.yaml index fa99157..f1f7c10 100644 --- a/spec/ndx-ophys-devices.namespace.yaml +++ b/spec/ndx-ophys-devices.namespace.yaml @@ -8,5 +8,7 @@ namespaces: name: ndx-ophys-devices schema: - namespace: core + neurodata_types: + - Device - source: ndx-ophys-devices.extensions.yaml version: 0.1.0 diff --git a/src/pynwb/ndx_ophys_devices/__init__.py b/src/pynwb/ndx_ophys_devices/__init__.py index 9ebf147..53a2422 100644 --- a/src/pynwb/ndx_ophys_devices/__init__.py +++ b/src/pynwb/ndx_ophys_devices/__init__.py @@ -18,17 +18,18 @@ # Load the namespace load_namespaces(str(__spec_path)) -# TODO: Define your classes here to make them accessible at the package level. -# Either have PyNWB generate a class from the spec using `get_class` as shown -# below or write a custom class and register it using the class decorator -# `@register_class("TetrodeSeries", "ndx-ophys-devices")` -TetrodeSeries = get_class("TetrodeSeries", "ndx-ophys-devices") -# NOTE: `widgets/tetrode_series_widget.py` adds a "widget" -# attribute to the TetrodeSeries class. This attribute is used by NWBWidgets. -# Delete the `widgets` subpackage or the `tetrode_series_widget.py` module -# if you do not want to define a custom widget for your extension neurodata -# type. +DeviceModel = get_class("DeviceModel", "ndx-ophys-devices") +Indicator = get_class("Indicator", "ndx-ophys-devices") +OpticalFiber = get_class("OpticalFiber", "ndx-ophys-devices") +ExcitationSource = get_class("ExcitationSource", "ndx-ophys-devices") +PulsedExcitationSource = get_class("PulsedExcitationSource", "ndx-ophys-devices") +Photodetector = get_class("Photodetector", "ndx-ophys-devices") +DichroicMirror = get_class("DichroicMirror", "ndx-ophys-devices") +OpticalFilter = get_class("OpticalFilter", "ndx-ophys-devices") +BandOpticalFilter = get_class("BandOpticalFilter", "ndx-ophys-devices") +EdgeOpticalFilter = get_class("EdgeOpticalFilter", "ndx-ophys-devices") +ObjectiveLens = get_class("ObjectiveLens", "ndx-ophys-devices") +Effector = get_class("Effector", "ndx-ophys-devices") -# Remove these functions from the package del load_namespaces, get_class diff --git a/src/pynwb/ndx_ophys_devices/testing/__init__.py b/src/pynwb/ndx_ophys_devices/testing/__init__.py new file mode 100644 index 0000000..64da8df --- /dev/null +++ b/src/pynwb/ndx_ophys_devices/testing/__init__.py @@ -0,0 +1,29 @@ +from ._mock import ( + mock_DeviceModel, + mock_Indicator, + mock_Effector, + mock_OpticalFiber, + mock_Photodetector, + mock_DichroicMirror, + mock_OpticalFilter, + mock_BandOpticalFilter, + mock_EdgeOpticalFilter, + mock_ObjectiveLens, + mock_ExcitationSource, + mock_PulsedExcitationSource, +) + +__all__ = [ + "mock_DeviceModel", + "mock_Indicator", + "mock_Effector", + "mock_OpticalFiber", + "mock_Photodetector", + "mock_DichroicMirror", + "mock_OpticalFilter", + "mock_BandOpticalFilter", + "mock_EdgeOpticalFilter", + "mock_ObjectiveLens", + "mock_ExcitationSource", + "mock_PulsedExcitationSource", +] diff --git a/src/pynwb/ndx_ophys_devices/testing/_mock.py b/src/pynwb/ndx_ophys_devices/testing/_mock.py new file mode 100644 index 0000000..f81391d --- /dev/null +++ b/src/pynwb/ndx_ophys_devices/testing/_mock.py @@ -0,0 +1,273 @@ +from typing import Optional + +from pynwb.testing.mock.utils import name_generator + +import ndx_ophys_devices + + +def mock_DeviceModel( + *, + name: Optional[str] = None, + description: str = "This is a mock instance of a DeviceModel type to be used for rapid testing.", + manufacturer: str = "A fake manufacturer of the mock DeviceModel.", + model: str = "A fake model of the mock DeviceModel.", +) -> ndx_ophys_devices.DeviceModel: + device_model = ndx_ophys_devices.DeviceModel( + name=name or name_generator("DeviceModel"), + description=description, + manufacturer=manufacturer, + model=model, + ) + return device_model + + +def mock_Indicator( + *, + name: Optional[str] = None, + description: str = "This is a mock instance of a Indicator type to be used for rapid testing.", + manufacturer: str = "A fake manufacturer of the mock indicator.", + label: str = "A fake label of the indicator.", + injection_brain_region: str = "A fake injection brain region of the indicator.", + injection_coordinates_in_mm: list = [3.0, 2.0, 1.0], +) -> ndx_ophys_devices.Indicator: + indicator = ndx_ophys_devices.Indicator( + name=name or name_generator("Indicator"), + description=description, + manufacturer=manufacturer, + label=label, + injection_brain_region=injection_brain_region, + injection_coordinates_in_mm=injection_coordinates_in_mm, + ) + return indicator + + +def mock_Effector( + *, + name: Optional[str] = None, + description: str = "This is a mock instance of a Effector type to be used for rapid testing.", + manufacturer: str = "A fake manufacturer of the mock effector.", + label: str = "A fake label of the effector.", + injection_brain_region: str = "A fake injection brain region of the effector.", + injection_coordinates_in_mm: list = [3.0, 2.0, 1.0], +) -> ndx_ophys_devices.Effector: + effector = ndx_ophys_devices.Effector( + name=name or name_generator("Effector"), + description=description, + manufacturer=manufacturer, + label=label, + injection_brain_region=injection_brain_region, + injection_coordinates_in_mm=injection_coordinates_in_mm, + ) + return effector + + +def mock_OpticalFiber( + *, + name: Optional[str] = None, + description: str = "This is a mock instance of a OpticalFiber type to be used for rapid testing.", + manufacturer: str = "A fake manufacturer of the mock an optical fiber.", + model: str = "A fake model of the mock an optical fiber.", + numerical_aperture: float = 0.2, + core_diameter_in_um: float = 400.0, +) -> ndx_ophys_devices.OpticalFiber: + optical_fiber = ndx_ophys_devices.OpticalFiber( + name=name or name_generator("OpticalFiber"), + description=description, + manufacturer=manufacturer, + model=model, + numerical_aperture=numerical_aperture, + core_diameter_in_um=core_diameter_in_um, + ) + return optical_fiber + + +def mock_Photodetector( + *, + name: Optional[str] = None, + description: str = "This is a mock instance of a Photodetector type to be used for rapid testing.", + manufacturer: str = "A fake manufacturer of the mock an photodetector.", + model: str = "A fake model of the mock an photodetector.", + detector_type: str = "PMT", + detected_wavelength_in_nm: float = 520.0, + gain: float = 100.0, + gain_unit: str = "A/W", +) -> ndx_ophys_devices.Photodetector: + photodetector = ndx_ophys_devices.Photodetector( + name=name or name_generator("Photodetector"), + description=description, + manufacturer=manufacturer, + model=model, + detector_type=detector_type, + detected_wavelength_in_nm=detected_wavelength_in_nm, + gain=gain, + gain_unit=gain_unit, + ) + return photodetector + + +def mock_DichroicMirror( + *, + name: Optional[str] = None, + description: str = "This is a mock instance of a DichroicMirror type to be used for rapid testing.", + manufacturer: str = "A fake manufacturer of the mock an dichroic mirror.", + model: str = "A fake model of the mock an dichroic mirror.", + cut_on_wavelength_in_nm: float = 470.0, + cut_off_wavelength_in_nm: float = 500.0, + reflection_band_in_nm: list = [460.0, 480.0], + transmission_band_in_nm: list = [490.0, 520.0], + angle_of_incidence_in_degrees: float = 45.0, +) -> ndx_ophys_devices.DichroicMirror: + dichroic_mirror = ndx_ophys_devices.DichroicMirror( + name=name or name_generator("DichroicMirror"), + description=description, + manufacturer=manufacturer, + model=model, + cut_on_wavelength_in_nm=cut_on_wavelength_in_nm, + cut_off_wavelength_in_nm=cut_off_wavelength_in_nm, + reflection_band_in_nm=reflection_band_in_nm, + transmission_band_in_nm=transmission_band_in_nm, + angle_of_incidence_in_degrees=angle_of_incidence_in_degrees, + ) + return dichroic_mirror + + +def mock_OpticalFilter( + *, + name: Optional[str] = None, + description: str = "This is a mock instance of a OpticalFilter type to be used for rapid testing.", + manufacturer: str = "A fake manufacturer of the mock an optical filter.", + model: str = "A fake model of the mock an optical filter.", + filter_type: str = "Longpass", +) -> ndx_ophys_devices.EdgeOpticalFilter: + optical_filter = ndx_ophys_devices.OpticalFilter( + name=name or name_generator("OpticalFilter"), + description=description, + manufacturer=manufacturer, + model=model, + filter_type=filter_type, + ) + return optical_filter + + +def mock_BandOpticalFilter( + *, + name: Optional[str] = None, + description: str = "This is a mock instance of a BandOpticalFilter type to be used for rapid testing.", + manufacturer: str = "A fake manufacturer of the mock an band optical filter.", + model: str = "A fake model of the mock an band optical filter.", + center_wavelength_in_nm: float = 505.0, + bandwidth_in_nm: float = 30.0, # 505±15nm + filter_type: str = "Bandpass", +) -> ndx_ophys_devices.BandOpticalFilter: + band_optical_filter = ndx_ophys_devices.BandOpticalFilter( + name=name or name_generator("BandOpticalFilter"), + description=description, + manufacturer=manufacturer, + model=model, + center_wavelength_in_nm=center_wavelength_in_nm, + bandwidth_in_nm=bandwidth_in_nm, + filter_type=filter_type, + ) + return band_optical_filter + + +def mock_EdgeOpticalFilter( + *, + name: Optional[str] = None, + description: str = "This is a mock instance of a EdgeOpticalFilter type to be used for rapid testing.", + manufacturer: str = "A fake manufacturer of the mock an edge optical filter.", + model: str = "A fake model of the mock an edge optical filter.", + cut_wavelength_in_nm: float = 585.0, + slope_in_percent_cut_wavelength: float = 1.0, + slope_starting_transmission_in_percent: float = 10.0, + slope_ending_transmission_in_percent: float = 80.0, + filter_type: str = "Longpass", +) -> ndx_ophys_devices.EdgeOpticalFilter: + edge_optical_filter = ndx_ophys_devices.EdgeOpticalFilter( + name=name or name_generator("EdgeOpticalFilter"), + description=description, + manufacturer=manufacturer, + model=model, + cut_wavelength_in_nm=cut_wavelength_in_nm, + slope_in_percent_cut_wavelength=slope_in_percent_cut_wavelength, + slope_starting_transmission_in_percent=slope_starting_transmission_in_percent, + slope_ending_transmission_in_percent=slope_ending_transmission_in_percent, + filter_type=filter_type, + ) + return edge_optical_filter + + +def mock_ObjectiveLens( + *, + name: Optional[str] = None, + description: str = "This is a mock instance of a ObjectiveLens type to be used for rapid testing.", + manufacturer: str = "A fake manufacturer of the mock an objective lens.", + model: str = "A fake model of the mock an objective lens.", + numerical_aperture: float = 0.2, + magnification: float = 10.0, +) -> ndx_ophys_devices.ObjectiveLens: + objective_lens = ndx_ophys_devices.ObjectiveLens( + name=name or name_generator("ObjectiveLens"), + description=description, + manufacturer=manufacturer, + model=model, + numerical_aperture=numerical_aperture, + magnification=magnification, + ) + return objective_lens + + +def mock_ExcitationSource( + *, + name: Optional[str] = None, + description: str = "This is a mock instance of a ExcitationSource type to be used for rapid testing.", + manufacturer: str = "A fake manufacturer of the mock excitation source.", + model: str = "A fake model of the mock excitation source.", + illumination_type: str = "Laser.", + excitation_wavelength_in_nm: float = 500.0, + power_in_W: float = 0.7, + intensity_in_W_per_m2: float = 0.005, + exposure_time_in_s: float = 2.51e-13, +) -> ndx_ophys_devices.ExcitationSource: + excitation_source = ndx_ophys_devices.ExcitationSource( + name=name or name_generator("ExcitationSource"), + description=description, + manufacturer=manufacturer, + model=model, + illumination_type=illumination_type, + excitation_wavelength_in_nm=excitation_wavelength_in_nm, + power_in_W=power_in_W, + intensity_in_W_per_m2=intensity_in_W_per_m2, + exposure_time_in_s=exposure_time_in_s, + ) + return excitation_source + + +def mock_PulsedExcitationSource( + *, + name: Optional[str] = None, + description: str = "This is a mock instance of a PulsedExcitationSource type to be used for rapid testing.", + manufacturer: str = "A fake manufacturer of the mock excitation source.", + model: str = "A fake model of the mock excitation source.", + illumination_type: str = "Laser.", + excitation_wavelength_in_nm: float = 500.0, + peak_power_in_W: float = 0.7, + peak_pulse_energy_in_J: float = 0.7, + intensity_in_W_per_m2: float = 0.005, + exposure_time_in_s: float = 2.51e-13, + pulse_rate_in_Hz: float = 2.0e6, +) -> ndx_ophys_devices.PulsedExcitationSource: + pulsed_excitation_source = ndx_ophys_devices.PulsedExcitationSource( + name=name or name_generator("PulsedExcitationSource"), + description=description, + manufacturer=manufacturer, + model=model, + illumination_type=illumination_type, + excitation_wavelength_in_nm=excitation_wavelength_in_nm, + peak_power_in_W=peak_power_in_W, + peak_pulse_energy_in_J=peak_pulse_energy_in_J, + intensity_in_W_per_m2=intensity_in_W_per_m2, + exposure_time_in_s=exposure_time_in_s, + pulse_rate_in_Hz=pulse_rate_in_Hz, + ) + return pulsed_excitation_source \ No newline at end of file diff --git a/src/pynwb/tests/test_constructors.py b/src/pynwb/tests/test_constructors.py new file mode 100644 index 0000000..b1d452b --- /dev/null +++ b/src/pynwb/tests/test_constructors.py @@ -0,0 +1,70 @@ +"""Test in-memory Python API constructors for ndx-ophys-devices extension.""" + +import pytest + +from ndx_ophys_devices.testing import ( + mock_DeviceModel, + mock_Indicator, + mock_Effector, + mock_OpticalFiber, + mock_Photodetector, + mock_DichroicMirror, + mock_OpticalFilter, + mock_BandOpticalFilter, + mock_EdgeOpticalFilter, + mock_ObjectiveLens, + mock_ExcitationSource, + mock_PulsedExcitationSource, +) + + +def test_constructor_device_model(): + mock_DeviceModel() + + +def test_constructor_indicator(): + mock_Indicator() + + +def test_constructor_effector(): + mock_Effector() + + +def test_constructor_optical_fiber(): + mock_OpticalFiber() + + +def test_constructor_photodetector(): + mock_Photodetector() + + +def test_constructor_dichroic_mirror(): + mock_DichroicMirror() + + +def test_constructor_optical_filter(): + mock_OpticalFilter() + + +def test_constructor_band_optical_filter(): + mock_BandOpticalFilter() + + +def test_constructor_edge_optical_filter(): + mock_EdgeOpticalFilter() + + +def test_constructor_objective_lens(): + mock_ObjectiveLens() + + +def test_constructor_excitation_source(): + mock_ExcitationSource() + + +def test_constructor_pulsed_excitation_source(): + mock_PulsedExcitationSource() + + +if __name__ == "__main__": + pytest.main() # Required since not a typical package structure diff --git a/src/pynwb/tests/test_ophysdevices.py b/src/pynwb/tests/test_ophysdevices.py new file mode 100644 index 0000000..53ebfb1 --- /dev/null +++ b/src/pynwb/tests/test_ophysdevices.py @@ -0,0 +1,92 @@ +from pynwb import NWBFile +from pynwb.testing.mock.file import mock_NWBFile +import pytest + +from ndx_ophys_devices.testing import ( + mock_DeviceModel, + mock_Indicator, + mock_Effector, + mock_OpticalFiber, + mock_Photodetector, + mock_DichroicMirror, + mock_OpticalFilter, + mock_BandOpticalFilter, + mock_EdgeOpticalFilter, + mock_ObjectiveLens, + mock_ExcitationSource, + mock_PulsedExcitationSource, +) + + +def test_constructor_device_model(): + mock_DeviceModel() + + +def test_constructor_indicator(): + mock_Indicator() + + +def test_constructor_effector(): + mock_Effector() + + +def test_constructor_optical_fiber(): + mock_OpticalFiber() + + +def test_constructor_photodetector(): + mock_Photodetector() + + +def test_constructor_dichroic_mirror(): + mock_DichroicMirror() + + +def test_constructor_optical_filter(): + mock_OpticalFilter() + + +def test_constructor_band_optical_filter(): + mock_BandOpticalFilter() + + +def test_constructor_edge_optical_filter(): + mock_EdgeOpticalFilter() + + +def test_constructor_objective_lens(): + mock_ObjectiveLens() + + +def test_constructor_excitation_source(): + mock_ExcitationSource() + + +def test_constructor_pulsed_excitation_source(): + mock_PulsedExcitationSource() + + +@pytest.fixture(scope="module") +def nwbfile_with_ophys_devices(): + nwbfile = mock_NWBFile() + + mock_DeviceModel() + mock_Indicator() + mock_OpticalFiber() + mock_ExcitationSource() + mock_PulsedExcitationSource() + mock_Photodetector() + mock_DichroicMirror() + mock_OpticalFilter() + mock_BandOpticalFilter() + mock_EdgeOpticalFilter() + mock_Effector() + mock_ObjectiveLens() + + return nwbfile + + +def set_up_nwbfile(nwbfile: NWBFile = None): + """Create an NWBFile""" + nwbfile = nwbfile or mock_NWBFile() + return nwbfile diff --git a/src/pynwb/tests/test_tetrodeseries.py b/src/pynwb/tests/test_tetrodeseries.py deleted file mode 100644 index 0e14040..0000000 --- a/src/pynwb/tests/test_tetrodeseries.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Unit and integration tests for the example TetrodeSeries extension neurodata type. - -TODO: Modify these tests to test your extension neurodata type. -""" - -import numpy as np - -from pynwb import NWBHDF5IO, NWBFile -from pynwb.testing.mock.device import mock_Device -from pynwb.testing.mock.ecephys import mock_ElectrodeGroup, mock_ElectrodeTable -from pynwb.testing.mock.file import mock_NWBFile -from pynwb.testing import TestCase, remove_test_file, NWBH5IOFlexMixin - -from ndx_ophys_devices import TetrodeSeries - - -def set_up_nwbfile(nwbfile: NWBFile = None): - """Create an NWBFile with a Device, ElectrodeGroup, and 10 electrodes in the ElectrodeTable.""" - nwbfile = nwbfile or mock_NWBFile() - device = mock_Device(nwbfile=nwbfile) - electrode_group = mock_ElectrodeGroup(device=device, nwbfile=nwbfile) - _ = mock_ElectrodeTable(n_rows=10, group=electrode_group, nwbfile=nwbfile) - - return nwbfile - - -class TestTetrodeSeriesConstructor(TestCase): - """Simple unit test for creating a TetrodeSeries.""" - - def setUp(self): - """Set up an NWB file. Necessary because TetrodeSeries requires references to electrodes.""" - self.nwbfile = set_up_nwbfile() - - def test_constructor(self): - """Test that the constructor for TetrodeSeries sets values as expected.""" - all_electrodes = self.nwbfile.create_electrode_table_region( - region=list(range(0, 10)), - description="all the electrodes", - ) - - data = np.random.rand(100, 10) - tetrode_series = TetrodeSeries( - name="name", - description="description", - data=data, - rate=1000.0, - electrodes=all_electrodes, - trode_id=1, - ) - - self.assertEqual(tetrode_series.name, "name") - self.assertEqual(tetrode_series.description, "description") - np.testing.assert_array_equal(tetrode_series.data, data) - self.assertEqual(tetrode_series.rate, 1000.0) - self.assertEqual(tetrode_series.starting_time, 0) - self.assertEqual(tetrode_series.electrodes, all_electrodes) - self.assertEqual(tetrode_series.trode_id, 1) - - -class TestTetrodeSeriesSimpleRoundtrip(TestCase): - """Simple roundtrip test for TetrodeSeries.""" - - def setUp(self): - self.nwbfile = set_up_nwbfile() - self.path = "test.nwb" - - def tearDown(self): - remove_test_file(self.path) - - def test_roundtrip(self): - """ - Add a TetrodeSeries to an NWBFile, write it to file, read the file, and test that the TetrodeSeries from the - file matches the original TetrodeSeries. - """ - all_electrodes = self.nwbfile.create_electrode_table_region( - region=list(range(0, 10)), - description="all the electrodes", - ) - - data = np.random.rand(100, 10) - tetrode_series = TetrodeSeries( - name="TetrodeSeries", - description="description", - data=data, - rate=1000.0, - electrodes=all_electrodes, - trode_id=1, - ) - - self.nwbfile.add_acquisition(tetrode_series) - - with NWBHDF5IO(self.path, mode="w") as io: - io.write(self.nwbfile) - - with NWBHDF5IO(self.path, mode="r", load_namespaces=True) as io: - read_nwbfile = io.read() - self.assertContainerEqual(tetrode_series, read_nwbfile.acquisition["TetrodeSeries"]) - - -class TestTetrodeSeriesRoundtripPyNWB(NWBH5IOFlexMixin, TestCase): - """Complex, more complete roundtrip test for TetrodeSeries using pynwb.testing infrastructure.""" - - def getContainerType(self): - return "TetrodeSeries" - - def addContainer(self): - set_up_nwbfile(self.nwbfile) - - all_electrodes = self.nwbfile.create_electrode_table_region( - region=list(range(0, 10)), - description="all the electrodes", - ) - - data = np.random.rand(100, 10) - tetrode_series = TetrodeSeries( - name="TetrodeSeries", - description="description", - data=data, - rate=1000.0, - electrodes=all_electrodes, - trode_id=1, - ) - self.nwbfile.add_acquisition(tetrode_series) - - def getContainer(self, nwbfile: NWBFile): - return nwbfile.acquisition["TetrodeSeries"] diff --git a/src/spec/create_extension_spec.py b/src/spec/create_extension_spec.py deleted file mode 100644 index 5c63af0..0000000 --- a/src/spec/create_extension_spec.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -import os.path - -from pynwb.spec import NWBNamespaceBuilder, export_spec, NWBGroupSpec, NWBAttributeSpec - -# TODO: import other spec classes as needed -# from pynwb.spec import NWBDatasetSpec, NWBLinkSpec, NWBDtypeSpec, NWBRefSpec - - -def main(): - # these arguments were auto-generated from your cookiecutter inputs - ns_builder = NWBNamespaceBuilder( - name="""ndx-ophys-devices""", - version="""0.1.0""", - doc="""This is an NWB extension for storing metadata of devices used in optical experimental setup (microscopy, fiber photometry, optogenetic stimulation etc.)""", - author=[ - "Alessandra Trapani", - ], - contact=[ - "alessandra.trapani@catalystneuro.com", - ], - ) - ns_builder.include_namespace("core") - - # TODO: if your extension builds on another extension, include the namespace - # of the other extension below - # ns_builder.include_namespace("ndx-other-extension") - - # TODO: define your new data types - # see https://pynwb.readthedocs.io/en/stable/tutorials/general/extensions.html - # for more information - tetrode_series = NWBGroupSpec( - neurodata_type_def="TetrodeSeries", - neurodata_type_inc="ElectricalSeries", - doc="An extension of ElectricalSeries to include the tetrode ID for each time series.", - attributes=[NWBAttributeSpec(name="trode_id", doc="The tetrode ID.", dtype="int32")], - ) - - # TODO: add all of your new data types to this list - new_data_types = [tetrode_series] - - # export the spec to yaml files in the spec folder - output_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "spec")) - export_spec(ns_builder, new_data_types, output_dir) - - -if __name__ == "__main__": - # usage: python create_extension_spec.py - main()