Skip to content

Commit

Permalink
Add extension
Browse files Browse the repository at this point in the history
  • Loading branch information
rly committed Mar 12, 2020
1 parent dd9a4c9 commit 1791ddd
Show file tree
Hide file tree
Showing 12 changed files with 749 additions and 28 deletions.
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,64 @@

## Installation

TODO:
```
pip install ndx-events
```

## Usage

```python
from pynwb import NWBFile, NWBHDF5IO
from ndx_events import LabeledEvents, AnnotatedEvents

from datetime import datetime
import numpy as np

nwb = NWBFile(
session_description='session_description',
identifier='identifier',
session_start_time=datetime.now().astimezone()
)

events = LabeledEvents(
name='my_events',
description='events from my experiment',
timestamps=[0., 1., 2.],
resolution=1e-5,
label_keys=np.uint([3, 4, 3]),
labels=['', '', '', 'event1', 'event2']
)
nwb.add_acquisition(events)

annotated_events = AnnotatedEvents(
name='my_annotated_events',
description='annotated events from my experiment',
resolution=1e-5
)
annotated_events.add_column(
name='extra',
description='extra metadata for each event type'
)
annotated_events.add_event_type(
label='Reward',
event_description='Times when the animal received juice reward.',
event_times=[1., 2., 3.],
extra='extra',
id=3
)
nwb.create_processing_module(name='events', description='processed event data')
nwb.processing['events'].add(annotated_events)

# Write nwb file
filename = 'example_usage.nwb'
with NWBHDF5IO(filename, 'w') as io:
io.write(nwb)

# Read nwb file and check its content
with NWBHDF5IO(filename, 'r', load_namespaces=True) as io:
nwb = io.read()
print(nwb)
```

This extension was created using [ndx-template](https://github.com/nwb-extensions/ndx-template).
Binary file added example_usage.nwb
Binary file not shown.
100 changes: 94 additions & 6 deletions spec/ndx-events.extensions.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,96 @@
groups:
- neurodata_type_def: TetrodeSeries
neurodata_type_inc: ElectricalSeries
doc: An extension of ElectricalSeries to include the tetrode ID for each time series.
- neurodata_type_def: Events
neurodata_type_inc: NWBDataInterface
doc: A list of timestamps, stored in seconds, of an event.
attributes:
- name: trode_id
dtype: int32
doc: The tetrode ID.
- name: description
dtype: text
doc: Description of the event.
datasets:
- name: timestamps
dtype: float32
dims:
- num_events
shape:
- null
doc: Event timestamps, in seconds, relative to the common experiment master-clock
stored in NWBFile.timestamps_reference_time.
attributes:
- name: unit
dtype: text
value: seconds
doc: Unit of measurement for timestamps, which is fixed to 'seconds'.
- name: resolution
dtype: float32
doc: The smallest possible difference between two event times. Usually 1 divided
by the event time sampling rate on the data acquisition system.
required: false
- neurodata_type_def: LabeledEvents
neurodata_type_inc: Events
doc: A list of timestamps, stored in seconds, of an event that can have different
labels. For example, this type could represent the times that reward was given,
as well as which of three different types of reward was given. In this case, the
'label_keys' dataset would contain values {0, 1, 2}, and the 'labels' dataset
would contain three text elements, where the first (index 0) specifies the name
of the reward associated with a label_keys = 0, the second (index 1) specifies
the name of the reward associated with a label_keys = 1, etc. The labels do not
have to start at 0 and do not need to be sequential, e.g. the 'label_keys' dataset
could contain values {0, 10, 100}, and the 'labels' dataset could contain 101
values, where labels[0] is 'No reward', labels[10] is '10% reward', labels[100]
is 'Full reward', and all other entries in 'labels' are the empty string.
datasets:
- name: label_keys
dtype: uint8
dims:
- num_events
shape:
- null
doc: Integer labels that map onto strings using the mapping in the 'labels' dataset.
Values must be 0 or greater and need not be sequential. This dataset should
have the same number of elements as the 'timestamps' dataset.
- name: labels
dtype: text
dims:
- num_labels
shape:
- null
doc: Mapping from an integer (the zero-based index) to a string, used to understand
the integer values in the 'label_keys' dataset. Use an empty string to represent
a label value that is not mapped to any text. Use '' to represent any values
that are None or empty.
- neurodata_type_def: TTLs
neurodata_type_inc: LabeledEvents
doc: Data type to hold timestamps of TTL pulses. The 'label_keys' dataset contains
the integer pulse values, and the 'labels' dataset contains user-defined labels
associated with each pulse value. The value at index n of the 'labels' dataset
corresponds to a pulse value of n. For example, the first value (index 0) of the
'labels' dataset corresponds to a pulse value of 0. See the LabeledEvents type
for more details.
- neurodata_type_def: AnnotatedEvents
neurodata_type_inc: DynamicTable
doc: Table to hold event timestamps and event metadata relevant to data preprocessing
and analysis. Each row corresponds to a different event type. Use the 'event_time'
dataset to store timestamps for each event type. Add user-defined columns to add
metadata for each event type or event time.
datasets:
- name: event_times_index
neurodata_type_inc: VectorIndex
doc: Index into the event_times dataset.
- name: event_times
neurodata_type_inc: VectorData
dtype: float32
doc: Event times for each event type.
attributes:
- name: resolution
dtype: float32
doc: The smallest possible difference between two event times. Usually 1 divided
by the event time sampling rate on the data acquisition system.
required: false
- name: label
neurodata_type_inc: VectorData
dtype: text
doc: Label for each event type.
- name: description
neurodata_type_inc: VectorData
dtype: text
doc: Description for each event type.
5 changes: 4 additions & 1 deletion spec/ndx-events.namespace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ namespaces:
schema:
- namespace: core
neurodata_types:
- ElectricalSeries
- NWBDataInterface
- DynamicTable
- VectorData
- VectorIndex
- source: ndx-events.extensions.yaml
version: 0.1.0
3 changes: 3 additions & 0 deletions src/pynwb/ndx_events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@

# Load the namespace
load_namespaces(ndx_events_specpath)

from . import io as __io # noqa: E402,F401
from .events import Events, LabeledEvents, TTLs, AnnotatedEvents # noqa: E402,F401
144 changes: 144 additions & 0 deletions src/pynwb/ndx_events/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import numpy as np

from pynwb import register_class
from pynwb.core import NWBDataInterface, DynamicTable
from hdmf.utils import docval, getargs, popargs, call_docval_func, get_docval


@register_class('Events', 'ndx-events')
class Events(NWBDataInterface):
"""
A list of timestamps, stored in seconds, of an event.
"""

__nwbfields__ = ('description',
'timestamps',
'resolution',
{'name': 'unit', 'settable': False})

@docval({'name': 'name', 'type': str, 'doc': 'The name of this Events object'}, # required
{'name': 'description', 'type': str, 'doc': 'The name of this Events object'}, # required
{'name': 'timestamps', 'type': ('array_data', 'data'), # required
'doc': ('Event timestamps, in seconds, relative to the common experiment master-clock '
'stored in NWBFile.timestamps_reference_time.'),
'shape': (None,)},
{'name': 'resolution', 'type': float,
'doc': ('The smallest possible difference between two event times. Usually 1 divided '
'by the event time sampling rate on the data acquisition system.'),
'default': None})
def __init__(self, **kwargs):
description, timestamps, resolution = popargs('description', 'timestamps', 'resolution', kwargs)
call_docval_func(super().__init__, kwargs)
self.description = description
self.timestamps = timestamps
self.resolution = resolution
self.fields['unit'] = 'seconds'


@register_class('LabeledEvents', 'ndx-events')
class LabeledEvents(Events):
"""
A list of timestamps, stored in seconds, of an event that can have different
labels. For example, this type could represent the times that reward was given,
as well as which of three different types of reward was given. In this case, the
'label_keys' dataset would contain values {0, 1, 2}, and the 'labels' dataset
would contain three text elements, where the first (index 0) specifies the name
of the reward associated with a label_keys = 0, the second (index 1) specifies
the name of the reward associated with a label_keys = 1, etc. The labels do not
have to start at 0 and do not need to be sequential, e.g. the 'label_keys' dataset
could contain values {0, 10, 100}, and the 'labels' dataset could contain 101
values, where labels[0] is 'No reward', labels[10] is '10% reward', labels[100]
is 'Full reward', and all other entries in 'labels' are the empty string.
"""

__nwbfields__ = ('label_keys',
'labels')

@docval(*get_docval(Events.__init__, 'name', 'description', 'timestamps'), # required
{'name': 'label_keys', 'type': ('array_data', 'data'), # required
'doc': ("Integer labels that map onto strings using the mapping in the 'labels' dataset. "
"Values must be 0 or greater and need not be sequential. This dataset should "
"have the same number of elements as the 'timestamps' dataset."),
'shape': (None,)},
{'name': 'labels', 'type': ('array_data', 'data'),
'doc': ("Mapping from an integer (the zero-based index) to a string, used to understand "
"the integer values in the 'label_keys' dataset. Use an empty string to represent "
"a label value that is not mapped to any text. Use '' to represent any values "
"that are None or empty. If the argument is not specified, the label "
"will be set to the string representation of the label keys and '' for other values."),
'shape': (None,), 'default': None},
*get_docval(Events.__init__, 'resolution'))
def __init__(self, **kwargs):
timestamps = getargs('timestamps', kwargs)
label_keys, labels = popargs('label_keys', 'labels', kwargs)
call_docval_func(super().__init__, kwargs)
if len(timestamps) != len(label_keys):
raise ValueError('Timestamps and label_keys must have the same length: %d != %d'
% (len(timestamps), len(label_keys)))
self.label_keys = label_keys
if labels is None:
unique_keys = np.unique(label_keys)
self.labels = [''] * (max(unique_keys) + 1)
for k in unique_keys:
self.labels[k] = str(k)
else:
if None in labels:
raise ValueError("None values are not allowed in the labels array. Please use '' for undefined label "
"keys.")
self.labels = labels


@register_class('TTLs', 'ndx-events')
class TTLs(LabeledEvents):
"""
Data type to hold timestamps of TTL pulses. The 'label_keys' dataset contains
the integer pulse values, and the 'labels' dataset contains user-defined labels
associated with each pulse value. The value at index n of the 'labels' dataset
corresponds to a pulse value of n. For example, the first value (index 0) of the
'labels' dataset corresponds to a pulse value of 0. See the LabeledEvents type
for more details.
"""
pass


@register_class('AnnotatedEvents', 'ndx-events')
class AnnotatedEvents(DynamicTable):
"""
Table to hold event timestamps and event metadata relevant to data preprocessing
and analysis. Each row corresponds to a different event type. Use the 'event_time'
dataset to store timestamps for each event type. Add user-defined columns to add
metadata for each event type or event time.
"""

__fields__ = (
'resolution',
)

__columns__ = (
{'name': 'event_times', 'description': 'Event times for each event type.', 'index': True},
{'name': 'label', 'description': 'Label for each event type.'},
{'name': 'event_description', 'description': 'Description for each event type.'}
# note that the name 'description' cannot be used because it is already an attribute on VectorData
)

@docval({'name': 'description', 'type': str, 'doc': 'Description of what is in this table'},
{'name': 'name', 'type': str, 'doc': 'Name of this AnnotatedEvents table', 'default': 'AnnotatedEvents'},
{'name': 'resolution', 'type': float,
'doc': ('The smallest possible difference between two event times. Usually 1 divided '
'by the event time sampling rate on the data acquisition system.'),
'default': None},
*get_docval(DynamicTable.__init__, 'id', 'columns', 'colnames'))
def __init__(self, **kwargs):
resolution = popargs('resolution', kwargs)
call_docval_func(super().__init__, kwargs)
self.resolution = resolution

@docval({'name': 'label', 'type': str, 'doc': 'Label for each event type.'},
{'name': 'event_description', 'type': str, 'doc': 'Description for each event type.'},
{'name': 'event_times', 'type': 'array_data', 'doc': 'Event times for each event type.', 'shape': (None,)},
{'name': 'id', 'type': int, 'doc': 'ID for each unit', 'default': None},
allow_extra=True)
def add_event_type(self, **kwargs):
"""Add an event type as a row to this table."""
# TODO columns do not exist and are hitting table.py line 377 for a name clash
super().add_row(**kwargs)
1 change: 1 addition & 0 deletions src/pynwb/ndx_events/io/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import events as __events # noqa: E402,F401
55 changes: 55 additions & 0 deletions src/pynwb/ndx_events/io/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from pynwb import register_map
from pynwb.io.core import NWBContainerMapper
from hdmf.common.io.table import DynamicTableMap
from hdmf.build import ObjectMapper, BuildManager
from hdmf.common import VectorData
from hdmf.utils import getargs, docval
from hdmf.spec import AttributeSpec

from ..events import Events, AnnotatedEvents


@register_map(Events)
class EventsMap(NWBContainerMapper):

def __init__(self, spec):
super().__init__(spec)
timestamps_spec = self.spec.get_dataset('timestamps')
self.map_spec('unit', timestamps_spec.get_attribute('unit'))
self.map_spec('resolution', timestamps_spec.get_attribute('resolution'))


@register_map(AnnotatedEvents)
class AnnotatedEventsMap(DynamicTableMap):

def __init__(self, spec):
super().__init__(spec)
event_times_spec = self.spec.get_dataset('event_times')
self.map_spec('resolution', event_times_spec.get_attribute('resolution'))

@DynamicTableMap.constructor_arg('resolution')
def resolution_carg(self, builder, manager):
if 'event_times' in builder:
return builder['event_times'].attributes.get('resolution')
return None


@register_map(VectorData)
class VectorDataMap(ObjectMapper):

# TODO fold this into pynwb.io.core.VectorDataMap

@docval({"name": "spec", "type": AttributeSpec, "doc": "the spec to get the attribute value for"},
{"name": "container", "type": VectorData, "doc": "the container to get the attribute value from"},
{"name": "manager", "type": BuildManager, "doc": "the BuildManager used for managing this build"},
returns='the value of the attribute')
def get_attr_value(self, **kwargs):
''' Get the value of the attribute corresponding to this spec from the given container '''
spec, container, manager = getargs('spec', 'container', 'manager', kwargs)

# handle custom mapping of container AnnotatedEvents.resolution -> spec AnnotatedEvents.event_times.resolution
if isinstance(container.parent, AnnotatedEvents):
if container.name == 'event_times':
if spec.name == 'resolution':
return container.parent.resolution
return super().get_attr_value(**kwargs)
Loading

0 comments on commit 1791ddd

Please sign in to comment.