Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
- Added new `ElectrodesTable` neurodata type. @mavaylon1 [#1890](https://github.com/NeurodataWithoutBorders/pynwb/pull/1890)
- Formally defined and renamed `ElectrodeTable` as the `ElectrodesTable` neurodata type. @mavaylon1 [#1890](https://github.com/NeurodataWithoutBorders/pynwb/pull/1890)
- Formally defined bands within `DecompositionSeries` as the neurodatatype `FrequencyBandsTable`. @mavaylon1 @rly [#2063](https://github.com/NeurodataWithoutBorders/pynwb/pull/2063)
- Added support for 2D `EventDetection.source_index` to indicate [time_index, channel_index]. @stephprince [#2091](https://github.com/NeurodataWithoutBorders/pynwb/pull/2091)
- Made `EventDetection.times` optional. @stephprince [#2091](https://github.com/NeurodataWithoutBorders/pynwb/pull/2091)
- Automatically add timezone information to timestamps reference time if no timezone information is specified. @stephprince [#2056](https://github.com/NeurodataWithoutBorders/pynwb/pull/2056)
- Added option to disable typemap caching and updated type map cache location. @stephprince [#2057](https://github.com/NeurodataWithoutBorders/pynwb/pull/2057)
- Added dictionary-like operations directly on `ProcessingModule` objects (e.g., `len(processing_module)`). @bendichter [#2020](https://github.com/NeurodataWithoutBorders/pynwb/pull/2020)
Expand Down
2 changes: 1 addition & 1 deletion docs/gallery/domain/ecephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@
name="threshold_events",
detection_method="thresholding, 1.5 * std",
source_electricalseries=raw_electrical_series,
source_idx=[1000, 2000, 3000],
source_idx=[[1000, 0], [2000, 4], [3000, 8]], # indicates the time and channel indices
times=[.033, .066, .099],
)

Expand Down
21 changes: 18 additions & 3 deletions src/pynwb/ecephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,15 +220,30 @@ class EventDetection(NWBDataInterface):
{'name': 'source_electricalseries', 'type': ElectricalSeries, 'doc': 'The source electrophysiology data'},
{'name': 'source_idx', 'type': ('array_data', 'data'),
'doc': 'Indices (zero-based) into source ElectricalSeries::data array corresponding '
'to time of event. Module description should define what is meant by time of event '
'(e.g., .25msec before action potential peak, zero-crossing time, etc). '
'to time of event or time and channel of event. For 1D arrays, specifies the time '
'index for each event. For 2D arrays with shape (num_events, 2), specifies '
'[time_index, channel_index] for each event. Module description should define what is meant '
'by time of event (e.g., .25msec before action potential peak, zero-crossing time, etc). '
'The index points to each event from the raw data'},
{'name': 'times', 'type': ('array_data', 'data'), 'doc': 'Timestamps of events, in Seconds'},
{'name': 'times', 'type': ('array_data', 'data'), 'doc': 'Timestamps of events, in Seconds',
'default': None},
{'name': 'name', 'type': str, 'doc': 'the name of this container', 'default': 'EventDetection'},
allow_positional=AllowPositional.WARNING,)
def __init__(self, **kwargs):
args_to_set = popargs_to_dict(('detection_method', 'source_electricalseries', 'source_idx', 'times'), kwargs)
super().__init__(**kwargs)

# Validate source_idx shape
source_idx = args_to_set['source_idx']
source_idx_shape = get_data_shape(source_idx, strict_no_data_load=True)
if source_idx_shape is not None:
if len(source_idx_shape) == 2 and source_idx_shape[1] != 2:
raise ValueError(f"EventDetection source_idx: 2D source_idx must have shape (num_events, 2) "
f"for [time_index, channel_index], but got shape {source_idx_shape}")
elif len(source_idx_shape) > 2:
raise ValueError(f"EventDetection source_idx: source_idx must be 1D or 2D array, "
f"but got {len(source_idx_shape)}D array with shape {source_idx_shape}")

for key, val in args_to_set.items():
setattr(self, key, val)
self.unit = 'seconds' # fixed value
Expand Down
2 changes: 1 addition & 1 deletion src/pynwb/nwb-schema
73 changes: 73 additions & 0 deletions tests/unit/test_ecephys.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,79 @@ def test_init(self):
self.assertEqual(eD.times, (0.1, 0.2, 0.3))
self.assertEqual(eD.unit, 'seconds')

def test_init_2d_source_idx(self):
"""Test EventDetection with 2D source_idx containing time and channel indices"""
data = np.random.rand(10, 2) # 10 time points, 2 channels
ts = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
_, region = self._create_table_and_region()
eS = ElectricalSeries(name='test_eS', data=data, electrodes=region, timestamps=ts)

# 2D source_idx with shape (num_events, 2) for [time_index, channel_index]
source_idx_2d = np.array([[1, 0], [2, 1], [3, 0],]) # 3 events
times = (0.1, 0.2, 0.3)

eD = EventDetection(detection_method='threshold detection',
source_electricalseries=eS,
source_idx=source_idx_2d,
times=times)

self.assertEqual(eD.detection_method, 'threshold detection')
self.assertEqual(eD.source_electricalseries, eS)
np.testing.assert_array_equal(eD.source_idx, source_idx_2d)
self.assertEqual(eD.times, times)
self.assertEqual(eD.unit, 'seconds')

def test_init_optional_times(self):
"""Test EventDetection with optional times parameter (times=None)"""
data = list(range(10))
ts = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
_, region = self._create_table_and_region()
eS = ElectricalSeries(name='test_eS', data=data, electrodes=region, timestamps=ts)

eD = EventDetection(detection_method='detection_method',
source_electricalseries=eS,
source_idx=(1, 2, 3))

self.assertEqual(eD.detection_method, 'detection_method')
self.assertEqual(eD.source_electricalseries, eS)
self.assertEqual(eD.source_idx, (1, 2, 3))
self.assertIsNone(eD.times)

def test_invalid_2d_source_idx_shape(self):
"""Test error handling for invalid 2D source_idx shapes"""
data = list(range(10))
ts = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
_, region = self._create_table_and_region()
eS = ElectricalSeries(name='test_eS', data=data, electrodes=region, timestamps=ts)

# Test with invalid 2D shape (num_events, 3) - should be (num_events, 2)
invalid_source_idx = np.array([[1, 0, 5], [2, 1, 6]])

msg = (f"EventDetection source_idx: 2D source_idx must have shape (num_events, 2) "
f"for [time_index, channel_index], but got shape {invalid_source_idx.shape}")
with self.assertRaisesWith(ValueError, msg):
EventDetection(detection_method='detection_method',
source_electricalseries=eS,
source_idx=invalid_source_idx,
times=(0.1, 0.2))

def test_invalid_3d_source_idx(self):
"""Test error handling for 3D source_idx arrays"""
data = list(range(10))
ts = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
_, region = self._create_table_and_region()
eS = ElectricalSeries(name='test_eS', data=data, electrodes=region, timestamps=ts)

# test with 3D array - should raise ValueError
invalid_source_idx = np.array([[[1, 0], [2, 1]], [[3, 0], [4, 1]]])

msg = (f"EventDetection source_idx: source_idx must be 1D or 2D array, "
f"but got {len(invalid_source_idx.shape)}D array with shape {invalid_source_idx.shape}")
with self.assertRaisesWith(ValueError, msg):
EventDetection(detection_method='detection_method',
source_electricalseries=eS,
source_idx=invalid_source_idx,
times=(0.1, 0.2))

class EventWaveformConstructor(TestCase):

Expand Down
Loading