diff --git a/docs/gallery/domain/ogen.py b/docs/gallery/domain/ogen.py new file mode 100644 index 000000000..1da48bc50 --- /dev/null +++ b/docs/gallery/domain/ogen.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +""" +Optogenetics +============ + + +This tutorial will demonstrate how to write optogenetics data. + +Creating and Writing NWB files +------------------------------ + +When creating a NWB file, the first step is to create the :py:class:`~pynwb.file.NWBFile` object. +""" + +from datetime import datetime +from uuid import uuid4 + +from dateutil.tz import tzlocal +from pynwb import NWBFile + +nwbfile = NWBFile( + session_description="my first synthetic recording", + identifier=str(uuid4()), + session_start_time=datetime.now(tzlocal()), + experimenter="Baggins, Bilbo", + lab="Bag End Laboratory", + institution="University of Middle Earth at the Shire", + experiment_description="I went on an adventure to reclaim vast treasures.", + session_id="LONELYMTN", +) + +#################### +# Adding optogenetic data +# ----------------------- +# The :py:mod:`~pynwb.ogen` module contains two data types that you will need to write optogenetics data, +# :py:class:`~pynwb.ogen.OptogeneticStimulusSite`, which contains metadata about the stimulus site, and +# :py:class:`~pynwb.ogen.OptogeneticSeries`, which contains the values of the time series. +# +# First, you need to create a :py:class:`~pynwb.device.Device` object linked to the :py:class:`~pynwb.file.NWBFile`: + +device = nwbfile.create_device( + name="device", + description="description of device", + manufacturer="optional but recommended", +) + +#################### +# Now, you can create an :py:class:`~pynwb.ogen.OptogeneticStimulusSite`. The easiest way to do this is to use the +# :py:meth:`~pynwb.file.NWBFile.create_ogen_site` method. + +ogen_site = nwbfile.create_ogen_site( + name="OptogeneticStimulusSite", + device=device, + description="This is an example optogenetic site.", + excitation_lambda=600.0, # nm + location="VISrl", +) + + +#################### +# Another equivalent approach would be to create a :py:class:`~pynwb.ogen.OptogeneticStimulusSite` and then add it to +# the :py:class:`~pynwb.file.NWBFile`: + +from pynwb.ogen import OptogeneticStimulusSite + +ogen_stim_site = OptogeneticStimulusSite( + name="OptogeneticStimulusSite2", + device=device, + description="This is an example optogenetic site.", + excitation_lambda=600.0, # nm + location="VISrl", +) + +nwbfile.add_ogen_site(ogen_stim_site) + +#################### +# The second approach is necessary if you have an extension of :py:class:`~pynwb.ogen.OptogeneticStimulusSite`. +# +# With the :py:class:`~pynwb.ogen.OptogeneticStimulusSite` added, you can now create a +# :py:class:`~pynwb.ogen.OptogeneticSeries`. Here, we will generate some random data using numpy and specify the +# timing using ``rate``. If you have samples at irregular intervals, you should use ``timestamps`` instead. + +import numpy as np +from pynwb.ogen import OptogeneticSeries + + +ogen_series = OptogeneticSeries( + name="OptogeneticSeries", + data=np.random.randn(20), + site=ogen_site, + rate=30.0, # Hz +) + +nwbfile.add_stimulus(ogen_series) + + diff --git a/docs/gallery/general/extensions.py b/docs/gallery/general/extensions.py index 1788076ea..1bc7a01d7 100644 --- a/docs/gallery/general/extensions.py +++ b/docs/gallery/general/extensions.py @@ -1,11 +1,11 @@ -''' +""" .. _tutorial-extending-nwb: Extending NWB ============= -The NWB format was designed to be easily extendable. Here we discuss some of the basic functionality -in PyNWB for creating Neurodata Extensions (NDX). +The NWB format was designed to be easily extendable. Here we discuss basic functionality +in PyNWB for creating Neurodata Extensions (NDX). .. seealso:: @@ -18,21 +18,19 @@ For more information on available tools for creating extensions, see :nwb_overview:`here `. -''' +.. _defining_extension: -#################### -# .. _defining_extension: -# -# Defining extensions -# ----------------------------------------------------- -# -# Extensions should be defined separately from the code that uses the extensions. This design decision is -# based on the assumption that the extension will be written once, and read or used multiple times. Here, we -# provide an example of how to create an extension for subsequent use. -# -# The following block of code demonstrates how to create a new namespace, and then add a new `neurodata_type` -# to this namespace. Finally, -# it calls :py:meth:`~hdmf.spec.write.NamespaceBuilder.export` to save the extensions to disk for downstream use. +Defining extensions +------------------- + +Extensions should be defined separately from the code that uses the extensions. This design decision is +based on the assumption that the extension will be written once, and read or used multiple times. Here, we +provide an example of how to create an extension for subsequent use. + +The following block of code demonstrates how to create a new namespace, and then add a new `neurodata_type` +to this namespace. Finally, it calls :py:meth:`~hdmf.spec.write.NamespaceBuilder.export` to save the extensions to +disk for downstream use. +""" # sphinx_gallery_thumbnail_path = 'figures/gallery_thumbnails_extensions.png' from pynwb.spec import NWBNamespaceBuilder, NWBGroupSpec, NWBAttributeSpec @@ -40,14 +38,18 @@ ns_path = "mylab.namespace.yaml" ext_source = "mylab.extensions.yaml" -ns_builder = NWBNamespaceBuilder('Extension for use in my Lab', "mylab", version='0.1.0') +ns_builder = NWBNamespaceBuilder( + "Extension for use in my Lab", "mylab", version="0.1.0" +) -ns_builder.include_type('ElectricalSeries', namespace='core') +ns_builder.include_type("ElectricalSeries", namespace="core") -ext = NWBGroupSpec('A custom ElectricalSeries for my lab', - attributes=[NWBAttributeSpec('trode_id', 'the tetrode id', 'int')], - neurodata_type_inc='ElectricalSeries', - neurodata_type_def='TetrodeSeries') +ext = NWBGroupSpec( + "A custom ElectricalSeries for my lab", + attributes=[NWBAttributeSpec("trode_id", "the tetrode id", "int")], + neurodata_type_inc="ElectricalSeries", + neurodata_type_def="TetrodeSeries", +) ns_builder.add_spec(ext_source, ext) ns_builder.export(ns_path) @@ -83,25 +85,22 @@ # # .. tip:: # -# Detailed documentation of all components and `neurodata_types` that are part of the core schema of NWB:N are +# Detailed documentation of all components and `neurodata_types` that are part of the core NWB schema are # available in the schema docs at `http://nwb-schema.readthedocs.io `_ . # Before creating a new type from scratch, please have a look at the schema docs to see if using or extending an # existing type may solve your problem. Also, the schema docs are helpful when extending an existing type to # better understand the design and structure of the neurodata_type you are using. - - -#################### +# # .. _using_extension: # # Using extensions -# ----------------------------------------------------- +# ---------------- # -# After an extension has been created, it can be used by downstream codes for reading and writing data. +# After an extension has been created, it can be used by downstream code for reading and writing data. # There are two main mechanisms for reading and writing extension data with PyNWB. # The first involves defining new :py:class:`~pynwb.core.NWBContainer` classes that are then mapped # to the neurodata types in the extension. - from pynwb import register_class, load_namespaces from pynwb.ecephys import ElectricalSeries from hdmf.utils import docval, get_docval, popargs @@ -110,15 +109,16 @@ load_namespaces(ns_path) -@register_class('TetrodeSeries', 'mylab') +@register_class("TetrodeSeries", "mylab") class TetrodeSeries(ElectricalSeries): + __nwbfields__ = ("trode_id",) - __nwbfields__ = ('trode_id',) - - @docval(*get_docval(ElectricalSeries.__init__) + ( - {'name': 'trode_id', 'type': int, 'doc': 'the tetrode id'},)) + @docval( + *get_docval(ElectricalSeries.__init__) + + ({"name": "trode_id", "type": int, "doc": "the tetrode id"},) + ) def __init__(self, **kwargs): - trode_id = popargs('trode_id', kwargs) + trode_id = popargs("trode_id", kwargs) super().__init__(**kwargs) self.trode_id = trode_id @@ -135,7 +135,6 @@ def __init__(self, **kwargs): # # If you do not want to write additional code to read your extensions, PyNWB is able to dynamically # create an :py:class:`~pynwb.core.NWBContainer` subclass for use within the PyNWB API. -# Dynamically created classes can be inspected using the built-in :py:mod:`inspect` module. from pynwb import get_class, load_namespaces @@ -143,7 +142,7 @@ def __init__(self, **kwargs): ns_path = "mylab.namespace.yaml" load_namespaces(ns_path) -AutoTetrodeSeries = get_class('TetrodeSeries', 'mylab') +TetrodeSeries = get_class("TetrodeSeries", "mylab") #################### # .. note:: @@ -151,12 +150,11 @@ def __init__(self, **kwargs): # When defining your own :py:class:`~pynwb.core.NWBContainer`, the subclass name does not need to be the same # as the extension type name. However, it is encouraged to keep class and extension names the same for the # purposes of readability. - -#################### +# # .. _caching_extension: # # Caching extensions to file -# ----------------------------------------------------- +# -------------------------- # # By default, extensions are cached to file so that your NWB file will carry the extensions needed to read the file # with it. @@ -167,49 +165,48 @@ def __init__(self, **kwargs): from dateutil.tz import tzlocal from pynwb import NWBFile -start_time = datetime(2017, 4, 3, 11, tzinfo=tzlocal()) -create_date = datetime(2017, 4, 15, 12, tzinfo=tzlocal()) -nwbfile = NWBFile('demonstrate caching', 'NWB456', start_time, - file_create_date=create_date) +nwbfile = NWBFile( + session_description="demonstrate caching", + identifier="NWB456", + session_start_time=datetime(2017, 4, 3, 11, tzinfo=tzlocal()), +) -device = nwbfile.create_device(name='trodes_rig123') +device = nwbfile.create_device(name="trodes_rig123") -electrode_name = 'tetrode1' -description = "an example tetrode" -location = "somewhere in the hippocampus" - -electrode_group = nwbfile.create_electrode_group(electrode_name, - description=description, - location=location, - device=device) +electrode_group = nwbfile.create_electrode_group( + name="tetrode1", + description="an example tetrode", + location="somewhere in the hippocampus", + device=device, +) for idx in [1, 2, 3, 4]: - nwbfile.add_electrode(id=idx, - x=1.0, y=2.0, z=3.0, - imp=float(-idx), - location='CA1', filtering='none', - group=electrode_group) -electrode_table_region = nwbfile.create_electrode_table_region([0, 2], 'the first and third electrodes') + nwbfile.add_electrode( + id=idx, + location="CA1", + group=electrode_group, + ) +electrode_table_region = nwbfile.create_electrode_table_region( + [0, 2], "the first and third electrodes" +) import numpy as np -rate = 10.0 np.random.seed(1234) data_len = 1000 data = np.random.rand(data_len * 2).reshape((data_len, 2)) -timestamps = np.arange(data_len) / rate - -ts = TetrodeSeries('test_ephys_data', - data, - electrode_table_region, - timestamps=timestamps, - trode_id=1, - # Alternatively, could specify starting_time and rate as follows - # starting_time=ephys_timestamps[0], - # rate=rate, - resolution=0.001, - comments="This data was randomly generated with numpy, using 1234 as the seed", - description="Random numbers generated with numpy.random.rand") + +ts = TetrodeSeries( + name="test_ephys_data", + data=data, + electrodes=electrode_table_region, + rate=10.0, + trode_id=1, + resolution=0.001, + comments="This data was randomly generated with numpy, using 1234 as the seed", + description="Random numbers generated with numpy.random.rand", +) + nwbfile.add_acquisition(ts) #################### @@ -223,28 +220,26 @@ def __init__(self, **kwargs): from pynwb import NWBHDF5IO -io = NWBHDF5IO('cache_spec_example.nwb', mode='w') -io.write(nwbfile) -io.close() +with NWBHDF5IO("cache_spec_example.nwb", mode="w") as io: + io.write(nwbfile) #################### # .. note:: # # For more information on writing NWB files, see :ref:`basic_writing`. - -#################### +# # By default, PyNWB does not use the namespaces cached in a file--you must # explicitly specify this. This behavior is enabled by the *load_namespaces* # argument to the :py:class:`~pynwb.NWBHDF5IO` constructor. -with NWBHDF5IO('cache_spec_example.nwb', mode='r', load_namespaces=True) as io: +with NWBHDF5IO("cache_spec_example.nwb", mode="r", load_namespaces=True) as io: nwbfile = io.read() #################### # .. _MultiContainerInterface: # # Creating and using a custom MultiContainerInterface -# ----------------------------------------------------- +# ---------------------------------------------------- # It is sometimes the case that we need a group to hold zero-or-more or # one-or-more of the same object. Here we show how to create an extension that # defines a group (`PotatoSack`) that holds multiple objects (`Potato`) and @@ -253,71 +248,88 @@ def __init__(self, **kwargs): from pynwb.spec import NWBNamespaceBuilder, NWBGroupSpec, NWBAttributeSpec -name = 'test_multicontainerinterface' -ns_path = name + ".namespace.yaml" -ext_source = name + ".extensions.yaml" - -ns_builder = NWBNamespaceBuilder(name + ' extensions', name, version='0.1.0') -ns_builder.include_type('NWBDataInterface', namespace='core') - -potato = NWBGroupSpec(neurodata_type_def='Potato', - neurodata_type_inc='NWBDataInterface', - doc='A potato', quantity='*', - attributes=[ - NWBAttributeSpec(name='weight', - doc='weight of potato', - dtype='float', - required=True), - NWBAttributeSpec(name='age', - doc='age of potato', - dtype='float', - required=False) - ]) - -potato_sack = NWBGroupSpec(neurodata_type_def='PotatoSack', - neurodata_type_inc='NWBDataInterface', - name='potato_sack', - doc='A sack of potatoes', quantity='?', - groups=[potato]) +ndx_name = "test_multicontainerinterface" +ns_path = ndx_name + ".namespace.yaml" +ext_source = ndx_name + ".extensions.yaml" + +ns_builder = NWBNamespaceBuilder(ndx_name + " extensions", ndx_name, version="0.1.0") +ns_builder.include_type("NWBDataInterface", namespace="core") + +potato = NWBGroupSpec( + neurodata_type_def="Potato", + neurodata_type_inc="NWBDataInterface", + doc="A potato", + quantity="*", + attributes=[ + NWBAttributeSpec( + name="weight", doc="weight of potato", dtype="float", required=True + ), + NWBAttributeSpec( + name="age", doc="age of potato", dtype="float", required=False + ), + ], +) + +potato_sack = NWBGroupSpec( + neurodata_type_def="PotatoSack", + neurodata_type_inc="NWBDataInterface", + name="potato_sack", + doc="A sack of potatoes", + quantity="?", + groups=[potato], +) ns_builder.add_spec(ext_source, potato_sack) ns_builder.export(ns_path) #################### # Then create Container classes registered to the new data types (this is -# generally done in a different file) +# generally done in a different file). You can do this manually: from pynwb import register_class, load_namespaces from pynwb.file import MultiContainerInterface, NWBContainer + load_namespaces(ns_path) -@register_class('Potato', name) +@register_class("Potato", ndx_name) class Potato(NWBContainer): - __nwbfields__ = ('name', 'weight', 'age') + __nwbfields__ = ("name", "weight", "age") - @docval({'name': 'name', 'type': str, 'doc': 'who names a potato?'}, - {'name': 'weight', 'type': float, 'doc': 'weight of potato in grams'}, - {'name': 'age', 'type': float, 'doc': 'age of potato in days'}) + @docval( + {"name": "name", "type": str, "doc": "who names a potato?"}, + {"name": "weight", "type": float, "doc": "weight of potato in grams"}, + {"name": "age", "type": float, "doc": "age of potato in days"}, + ) def __init__(self, **kwargs): - super().__init__(name=kwargs['name']) - self.weight = kwargs['weight'] - self.age = kwargs['age'] + super().__init__(name=kwargs["name"]) + self.weight = kwargs["weight"] + self.age = kwargs["age"] -@register_class('PotatoSack', name) +@register_class("PotatoSack", ndx_name) class PotatoSack(MultiContainerInterface): - __clsconf__ = { - 'attr': 'potatos', - 'type': Potato, - 'add': 'add_potato', - 'get': 'get_potato', - 'create': 'create_potato', + "attr": "potatos", + "type": Potato, + "add": "add_potato", + "get": "get_potato", + "create": "create_potato", } +#################### +# You can also generate these classes automatically. + +from pynwb import get_class, load_namespaces + +load_namespaces(ns_path) + +Potato = get_class("Potato", ndx_name) +PotatoSack = get_class("PotatoSack", ndx_name) + + #################### # Then use the objects (again, this would often be done in a different file). @@ -326,17 +338,21 @@ class PotatoSack(MultiContainerInterface): from dateutil.tz import tzlocal # You can add potatoes to a potato sack in different ways -potato_sack = PotatoSack(potatos=Potato(name='potato1', age=2.3, weight=3.0)) -potato_sack.add_potato(Potato('potato2', 3.0, 4.0)) -potato_sack.create_potato('big_potato', 10.0, 20.0) +potato_sack = PotatoSack(potatos=Potato(name="potato1", age=2.3, weight=3.0)) +potato_sack.add_potato(Potato("potato2", 3.0, 4.0)) +potato_sack.create_potato("big_potato", 10.0, 20.0) -nwbfile = NWBFile("a file with metadata", "NB123A", datetime(2018, 6, 1, tzinfo=tzlocal())) +nwbfile = NWBFile( + session_description="a file with metadata", + identifier="NB123A", + session_start_time=datetime(2018, 6, 1, tzinfo=tzlocal()), +) -pmod = nwbfile.create_processing_module('module_name', 'desc') +pmod = nwbfile.create_processing_module("module_name", "desc") pmod.add_container(potato_sack) -with NWBHDF5IO('test_multicontainerinterface.nwb', 'w') as io: +with NWBHDF5IO("test_multicontainerinterface.nwb", "w") as io: io.write(nwbfile) #################### @@ -345,17 +361,13 @@ class PotatoSack(MultiContainerInterface): load_namespaces(ns_path) # from xxx import PotatoSack, Potato -with NWBHDF5IO('test_multicontainerinterface.nwb', 'r') as io: +with NWBHDF5IO("test_multicontainerinterface.nwb", "r") as io: nwb = io.read() - print(nwb.get_processing_module()['potato_sack'].get_potato('big_potato').weight) -# note: you can call get_processing_module() with or without the module name as -# an argument. however, if there is more than one module, the name is required. -# here, there is more than one potato, so the name of the potato is required as -# an argument to get get_potato + print(nwb.processing["module_name"]["potato_sack"].get_potato("big_potato").weight) #################### # Example: Cortical Surface Mesh -# ----------------------------------------------------- +# ------------------------------ # # Here we show how to create extensions by creating a data class for a # cortical surface mesh. This data type is particularly important for ECoG data, we need to know where each electrode is @@ -370,7 +382,7 @@ class PotatoSack(MultiContainerInterface): from pynwb.spec import NWBDatasetSpec, NWBNamespaceBuilder, NWBGroupSpec -name = 'ecog' +name = "ecog" ns_path = name + ".namespace.yaml" ext_source = name + ".extensions.yaml" @@ -379,18 +391,31 @@ class PotatoSack(MultiContainerInterface): # class is `CorticalSurface`, and it requires two matrices, `vertices` and # `faces`. -surface = NWBGroupSpec(doc='brain cortical surface', - datasets=[ - NWBDatasetSpec(doc='faces for surface, indexes vertices', shape=(None, 3), - name='faces', dtype='uint', dims=('face_number', 'vertex_index')), - NWBDatasetSpec(doc='vertices for surface, points in 3D space', shape=(None, 3), - name='vertices', dtype='float', dims=('vertex_number', 'xyz'))], - neurodata_type_def='CorticalSurface', - neurodata_type_inc='NWBDataInterface') +surface = NWBGroupSpec( + doc="brain cortical surface", + datasets=[ + NWBDatasetSpec( + doc="faces for surface, indexes vertices", + shape=(None, 3), + name="faces", + dtype="uint", + dims=("face_number", "vertex_index"), + ), + NWBDatasetSpec( + doc="vertices for surface, points in 3D space", + shape=(None, 3), + name="vertices", + dtype="float", + dims=("vertex_number", "xyz"), + ), + ], + neurodata_type_def="CorticalSurface", + neurodata_type_inc="NWBDataInterface", +) # Now we set up the builder and add this object -ns_builder = NWBNamespaceBuilder(name + ' extensions', name, version='0.1.0') +ns_builder = NWBNamespaceBuilder(name + " extensions", name, version="0.1.0") ns_builder.add_spec(ext_source, surface) ns_builder.export(ns_path) @@ -431,23 +456,28 @@ class PotatoSack(MultiContainerInterface): from datetime import datetime import numpy as np -load_namespaces('ecog.namespace.yaml') -CorticalSurface = get_class('CorticalSurface', 'ecog') - -cortical_surface = CorticalSurface(vertices=[[0.0, 1.0, 1.0], - [1.0, 1.0, 2.0], - [2.0, 2.0, 1.0], - [2.0, 1.0, 1.0], - [1.0, 2.0, 1.0]], - faces=np.array([[0, 1, 2], [1, 2, 3]]).astype('uint'), - name='cortex') - -nwbfile = NWBFile('my first synthetic recording', 'EXAMPLE_ID', datetime.now()) - -cortex_module = nwbfile.create_processing_module(name='cortex', - description='description') +load_namespaces("ecog.namespace.yaml") +CorticalSurface = get_class("CorticalSurface", "ecog") + +cortical_surface = CorticalSurface( + vertices=[ + [0.0, 1.0, 1.0], + [1.0, 1.0, 2.0], + [2.0, 2.0, 1.0], + [2.0, 1.0, 1.0], + [1.0, 2.0, 1.0], + ], + faces=np.array([[0, 1, 2], [1, 2, 3]]).astype("uint"), + name="cortex", +) + +nwbfile = NWBFile("my first synthetic recording", "EXAMPLE_ID", datetime.now()) + +cortex_module = nwbfile.create_processing_module( + name="cortex", description="description" +) cortex_module.add_container(cortical_surface) -with NWBHDF5IO('test_cortical_surface.nwb', 'w') as io: +with NWBHDF5IO("test_cortical_surface.nwb", "w") as io: io.write(nwbfile)