diff --git a/{{ cookiecutter.namespace }}/NEXTSTEPS.md b/{{ cookiecutter.namespace }}/NEXTSTEPS.md index b92e465..fb4689f 100644 --- a/{{ cookiecutter.namespace }}/NEXTSTEPS.md +++ b/{{ cookiecutter.namespace }}/NEXTSTEPS.md @@ -41,6 +41,12 @@ replaced or removed. the file, and confirm the read data types are equal to the written data types) is highly encouraged. +7. Define custom visualization widgets for your new extension data types in +`src/pynwb/widgets` so that the visualizations can be displayed with +[nwbwidgets](https://github.com/NeurodataWithoutBorders/nwbwidgets). Register your widget +with your extension Python class in `src/pynwb/__init__.py` or wherever your extension +Python class is defined. + 8. You may need to modify `pyproject.toml` and re-run `python -m pip install -e .` if you use any dependencies. diff --git a/{{ cookiecutter.namespace }}/notebooks/example.ipynb b/{{ cookiecutter.namespace }}/notebooks/example.ipynb new file mode 100644 index 0000000..9480d5e --- /dev/null +++ b/{{ cookiecutter.namespace }}/notebooks/example.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "174c5018-1c0a-4f55-899d-049bb87f63d5", + "metadata": {}, + "source": [ + "# Example demonstration of the example TetrodeSeries extension neurodata type\n", + "\n", + "TODO: Update this notebook with an example usage of your extension" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02798a80-faea-4b75-aa97-70afad90fe27", + "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 {{ cookiecutter.py_pkg_name }} 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", + " 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" + ] + }, + { + "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`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dcd2c070-f2c9-4ffc-8637-25ec3e6f7b37", + "metadata": {}, + "outputs": [], + "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" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1363282d-1a32-447d-9ca3-fc2360785cc2", + "metadata": {}, + "outputs": [], + "source": [ + "from nwbwidgets import nwb2widget, default_neurodata_vis_spec\n", + "vis_spec = default_neurodata_vis_spec\n", + "vis_spec[TetrodeSeries] = TetrodeSeries.widget\n", + "nwb2widget(nwbfile, vis_spec)" + ] + }, + { + "cell_type": "markdown", + "id": "ac894fae-6c5e-4a3d-be1e-d158aef084a4", + "metadata": {}, + "source": [ + "Write the file with the extension neurodata type to disk" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fdaf978-6f95-4871-b26c-dad4bb62da42", + "metadata": {}, + "outputs": [], + "source": [ + "with NWBHDF5IO(\"test.nwb\", \"w\") as io:\n", + " io.write(nwbfile)" + ] + }, + { + "cell_type": "markdown", + "id": "a30660a3-8e64-4c5e-963d-6ecd7e31897e", + "metadata": {}, + "source": [ + "Read the NWB file from disk and print the `TetrodeSeries` object" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00e38084-b55e-4c7a-ac89-8b10e43f41c1", + "metadata": {}, + "outputs": [], + "source": [ + "with NWBHDF5IO(\"test.nwb\", \"r\") as io:\n", + " read_nwbfile = io.read()\n", + " print(read_nwbfile.acquisition[\"TetrodeSeries\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b36569b2-09d1-4281-bf4c-fb602e53636c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/{{ cookiecutter.namespace }}/requirements-dev.txt b/{{ cookiecutter.namespace }}/requirements-dev.txt index 66463c2..930f1e9 100644 --- a/{{ cookiecutter.namespace }}/requirements-dev.txt +++ b/{{ cookiecutter.namespace }}/requirements-dev.txt @@ -4,6 +4,7 @@ black==23.9.1 codespell==2.2.6 coverage==7.3.2 hdmf-docutils==0.4.5 +nwbwidgets==0.11.3 pre-commit==3.4.0 pytest==7.4.2 pytest-cov==4.1.0 diff --git a/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/__init__.py b/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/__init__.py index 9a72ad0..ac594fd 100644 --- a/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/__init__.py +++ b/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/__init__.py @@ -24,5 +24,22 @@ # `@register_class("TetrodeSeries", "{{ cookiecutter.namespace }}")` TetrodeSeries = get_class("TetrodeSeries", "{{ cookiecutter.namespace }}") +# If NWBWidgets is installed, create a custom widget for the TetrodeSeries +# neurodata type that displays the trode_id field +# Usage: +# from nwbwidgets import nwb2widget, default_neurodata_vis_spec +# vis_spec = default_neurodata_vis_spec +# vis_spec[TetrodeSeries] = TetrodeSeries.widget +# nwb2widget(nwbfile, vis_spec) +try: + from .widgets import TetrodeSeriesWidget + + # add the widget class to the TetrodeSeries class + TetrodeSeries.widget = TetrodeSeriesWidget + +except ImportError: + # NWBWidgets is not installed, so we cannot create a new widget + pass + # Remove these functions from the package del load_namespaces, get_class diff --git a/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/widgets/README.md b/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/widgets/README.md new file mode 100644 index 0000000..75caa7f --- /dev/null +++ b/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/widgets/README.md @@ -0,0 +1,3 @@ +Add widgets that define custom visualizations for your extension, so that +the visualizations can be displayed with +[nwbwidgets](https://github.com/NeurodataWithoutBorders/nwbwidgets). \ No newline at end of file diff --git a/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/widgets/__init__.py b/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/widgets/__init__.py new file mode 100644 index 0000000..cfaf4f8 --- /dev/null +++ b/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/widgets/__init__.py @@ -0,0 +1 @@ +from .tetrode_series_widget import TetrodeSeriesWidget # noqa: F401 diff --git a/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/widgets/tetrode_series_widget.py b/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/widgets/tetrode_series_widget.py new file mode 100644 index 0000000..ab229eb --- /dev/null +++ b/{{ cookiecutter.namespace }}/src/pynwb/{{ cookiecutter.py_pkg_name }}/widgets/tetrode_series_widget.py @@ -0,0 +1,27 @@ +from nwbwidgets.ecephys import ElectricalSeriesWidget +from ipywidgets import widgets + +from .. import TetrodeSeries + + +class TetrodeSeriesWidget(ElectricalSeriesWidget): # this is an HBox + def __init__(self, tetrode_series: TetrodeSeries, **kwargs): + super().__init__(electrical_series=tetrode_series, **kwargs) + vbox = widgets.VBox( + children=[ + self._create_trode_id_box(tetrode_series), + widgets.HBox(children=list(self.children)), + ] + ) + self.children = [vbox] + + def _create_trode_id_box(self, tetrode_series: TetrodeSeries): + field_lay = widgets.Layout( + max_height="40px", + max_width="600px", + min_height="30px", + min_width="130px", + ) + key = widgets.Label("trode_id:", layout=field_lay) + val = widgets.Label(str(tetrode_series.trode_id), layout=field_lay) + return widgets.HBox(children=[key, val])