From 93972b6b5e69f424ff478b75b9512cd62f3fa1c9 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 5 Aug 2026 05:35:12 -0700 Subject: [PATCH 1/2] Fix the Units waveforms test fixtures and assert the doubly indexed structure TestUnitsIO and TestUnitsWaveformsOnlyIO labelled the outer dimension of the 3-D waveforms input to add_unit as electrodes and the middle dimension as spike events. add_unit does the opposite: dim 0 becomes waveforms_index_index (spike events per unit) and dim 1 becomes waveforms_index (waveforms, one per electrode, per spike event), which matches the schema description of the doubly indexed waveforms column. Because of the swapped labelling the fixtures also contradicted their own spike_times. TestUnitsIO declared 3 spike_times per unit but encoded 2 and 3 spike events, and TestUnitsWaveformsOnlyIO declared 3 spike_times per unit but encoded 2 spike events. Neither test asserted anything about waveforms, so this was never caught. Reorder both fixtures to (num_spikes, num_electrodes, num_samples), give TestUnitsIO distinct sample values so a transposition is detectable, and add test_waveforms_structure asserting waveforms_index_index, waveforms_index, and the 2-D waveforms dataset after a roundtrip. --- tests/integration/hdf5/test_misc.py | 78 ++++++++++++++++------------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/tests/integration/hdf5/test_misc.py b/tests/integration/hdf5/test_misc.py index b07066a62..468031d81 100644 --- a/tests/integration/hdf5/test_misc.py +++ b/tests/integration/hdf5/test_misc.py @@ -14,40 +14,30 @@ class TestUnitsIO(AcquisitionH5IOMixin, TestCase): def setUpContainer(self): """ Return the test Units to read/write """ + # A 3-D waveforms input to add_unit is ordered (num_spikes, num_electrodes, num_samples). + # Dim 0 indexes spike events and becomes waveforms_index_index, dim 1 indexes the electrodes + # that observed each spike event and becomes waveforms_index, and dim 2 holds the samples of + # each waveform. Every unit below has one waveform per electrode per spike time, so the + # number of spike events matches the number of spike_times. ut = Units(name='UnitsTest', description='a simple table for testing Units') ut.add_unit(spike_times=[0., 1., 2.], obs_intervals=[[0., 1.], [2., 3.]], waveform_mean=[1., 2., 3.], waveform_sd=[4., 5., 6.], - waveforms=[ - [ # elec 1 - [1, 2, 3], - [1, 2, 3], - [1, 2, 3] - ], [ # elec 2 - [1, 2, 3], - [1, 2, 3], - [1, 2, 3] + waveforms=[ # 3 spike times, 2 electrodes, 3 samples + [ # spike 1 + [1, 2, 3], # elec 1, [sample 1, sample 2, sample 3] + [4, 5, 6] # elec 2 + ], [ # spike 2 + [7, 8, 9], + [10, 11, 12] + ], [ # spike 3 + [13, 14, 15], + [16, 17, 18] ] ]) ut.add_unit(spike_times=[3., 4., 5.], obs_intervals=[[2., 5.], [6., 7.]], waveform_mean=[1., 2., 3.], waveform_sd=[4., 5., 6.], - waveforms=np.array([ - [ # elec 1 - [1, 2, 3], # spike 1, [sample 1, sample 2, sample 3] - [1, 2, 3], # spike 2 - [1, 2, 3], # spike 3 - [1, 2, 3] # spike 4 - ], [ # elec 2 - [1, 2, 3], # spike 1 - [1, 2, 3], # spike 2 - [1, 2, 3], # spike 3 - [1, 2, 3] # spike 4 - ], [ # elec 3 - [1, 2, 3], # spike 1 - [1, 2, 3], # spike 2 - [1, 2, 3], # spike 3 - [1, 2, 3] # spike 4 - ] - ])) + # 3 spike times, 4 electrodes, 3 samples, continuing the sample values above + waveforms=np.arange(19, 55).reshape(3, 4, 3)) ut.waveform_rate = 40000. ut.resolution = 1/40000 return ut @@ -70,6 +60,24 @@ def test_get_obs_intervals(self): np.testing.assert_array_equal(received, [[2., 5.], [6., 7.]]) np.testing.assert_array_equal(ut['obs_intervals'][:], [[[0., 1.], [2., 3.]], [[2., 5.], [6., 7.]]]) + def test_waveforms_structure(self): + """ Test the structure of the doubly indexed waveforms column read from file """ + ut = self.roundtripContainer() + waveforms_index_index = ut['waveforms'] + waveforms_index = waveforms_index_index.target + waveforms = waveforms_index.target + + # waveforms_index_index holds the number of spike events of each unit + np.testing.assert_array_equal(waveforms_index_index.data[:], [3, 6]) + # waveforms_index holds the number of waveforms, one per electrode, of each spike event + np.testing.assert_array_equal(waveforms_index.data[:], [2, 4, 6, 10, 14, 18]) + # the waveforms dataset itself is 2-D, (num_waveforms, num_samples) + np.testing.assert_array_equal(waveforms.data[:], np.arange(1, 55).reshape(18, 3)) + + # unit 0 has 2 electrodes per spike event, unit 1 has 4 + self.assertEqual([len(spike_event) for spike_event in waveforms_index_index[0]], [2, 2, 2]) + self.assertEqual([len(spike_event) for spike_event in waveforms_index_index[1]], [4, 4, 4]) + class TestUnitsWaveformsOnlyIO(AcquisitionH5IOMixin, TestCase): """Test roundtripping waveform metadata when only waveforms are present.""" @@ -78,13 +86,14 @@ def setUpContainer(self): ut = Units(name='UnitsWaveformsOnlyTest', description='a simple table for testing Units waveforms') ut.add_unit( spike_times=[0., 1., 2.], - waveforms=[ - [ - [1, 2, 3], + waveforms=[ # 3 spike times, 2 electrodes, 3 samples + [ # spike 1 + [1, 2, 3], # elec 1, [sample 1, sample 2, sample 3] + [1, 2, 3] # elec 2 + ], [ # spike 2 [1, 2, 3], [1, 2, 3] - ], [ - [1, 2, 3], + ], [ # spike 3 [1, 2, 3], [1, 2, 3] ] @@ -92,13 +101,14 @@ def setUpContainer(self): ) ut.add_unit( spike_times=[3., 4., 5.], - waveforms=np.array([ + waveforms=np.array([ # 3 spike times, 2 electrodes, 3 samples [ - [1, 2, 3], [1, 2, 3], [1, 2, 3] ], [ [1, 2, 3], + [1, 2, 3] + ], [ [1, 2, 3], [1, 2, 3] ] From 5f4e6ffdc24568e91c61d867b211f606dd8bc6b2 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 5 Aug 2026 05:36:43 -0700 Subject: [PATCH 2/2] Add CHANGELOG entry for #2240 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b32056e99..309032cec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Fixed `mock_DeviceModel` defaulting `manufacturer` to `None`. The mock now defaults it to `"manufacturer"`. @HugoFara [#2232](https://github.com/NeurodataWithoutBorders/pynwb/pull/2232) - Fixed reading a file whose dates carry a sub-minute UTC offset (e.g. `1900-10-01T00:00:00-05:50:36`). @h-mayorquin [#2230](https://github.com/NeurodataWithoutBorders/pynwb/pull/2230) - Fixed wide pandas DataFrames in the tutorials spilling out of the content column and into the right margin. @bendichter [#2236](https://github.com/NeurodataWithoutBorders/pynwb/pull/2236) +- Fixed the `Units` waveforms test fixtures, which labelled the dimensions of the 3-D `add_unit` waveforms input as `(num_electrodes, num_spikes, num_samples)` when `add_unit` reads them as `(num_spikes, num_electrodes, num_samples)`, and so encoded a number of spike events that disagreed with the unit's `spike_times`. Added `TestUnitsIO.test_waveforms_structure` asserting `waveforms_index_index`, `waveforms_index`, and the 2-D `waveforms` dataset after a roundtrip. @adityasingh2400 [#2240](https://github.com/NeurodataWithoutBorders/pynwb/pull/2240) ## PyNWB 4.1.0 (July 23, 2026)