Skip to content

Commit e35a0d7

Browse files
authored
Merge pull request #1862 from h-mayorquin/add_intan_test
Add a real regression test for one-file-per-signal Intan digital streams
2 parents 28b60f3 + 89dccc5 commit e35a0d7

2 files changed

Lines changed: 54 additions & 18 deletions

File tree

neo/rawio/intanrawio.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -571,13 +571,15 @@ def _demultiplex_digital_data(self, raw_digital_data, channel_ids, i_start, i_st
571571
572572
"""
573573
dtype = np.uint16 # We fix this to match the memmap dtype
574+
# Slice to the requested window once up front so the bitwise unpacking below only runs over
575+
# the requested samples rather than the whole recording for every channel.
576+
raw_digital_data = raw_digital_data[i_start:i_stop]
574577
output = np.zeros((i_stop - i_start, len(channel_ids)), dtype=dtype)
575578

576579
for channel_index, channel_id in enumerate(channel_ids):
577580
native_order = self.native_channel_order[channel_id]
578581
mask = 1 << native_order
579-
demultiplex_data = np.bitwise_and(raw_digital_data, mask) > 0
580-
output[:, channel_index] = demultiplex_data[i_start:i_stop].flatten()
582+
output[:, channel_index] = (np.bitwise_and(raw_digital_data, mask) > 0).flatten()
581583

582584
return output
583585

neo/test/rawiotest/test_intanrawio.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class TestIntanRawIO(
1818
"intan/rhs_fpc_multistim_240514_082243/rhs_fpc_multistim_240514_082243.rhs", # Format header-attached newer version
1919
"intan/intan_fpc_test_231117_052630/info.rhd", # Format one-file-per-channel
2020
"intan/intan_fps_test_231117_052500/info.rhd", # Format one file per signal
21+
"intan/intan_fps_multiple_digital_channels/info.rhd", # one-file-per-signal with multiple packed digital channels (issue #1853)
2122
"intan/intan_fpc_rhs_test_240329_091637/info.rhs", # Format one-file-per-channel
2223
"intan/intan_fps_rhs_test_240329_091536/info.rhs", # Format one-file-per-signal
2324
"intan/rhd_fpc_multistim_240514_082044/info.rhd", # Multiple digital channels one-file-per-channel rhd
@@ -218,25 +219,58 @@ def test_correct_decoding_of_stimulus_current(self):
218219

219220
assert np.isclose(duration_of_positive_pulse, expected_duration)
220221

222+
def test_reading_one_file_per_signal_multiple_digital_channels(self):
223+
"Regression test for https://github.com/NeuralEnsemble/python-neo/issues/1853"
224+
# One-file-per-signal recording with 16 digital-input and 16 digital-output channels.
225+
# All channels of a digital stream are packed as bit positions of a single 16-bit word
226+
# per sample, so the per-file sample count must divide by one word, not by the channel
227+
# count. Earlier fixtures had a single digital channel per stream, so the division by one
228+
# happened to be correct and never exercised this path.
229+
file_path = Path(self.get_local_path("intan/intan_fps_multiple_digital_channels/info.rhd"))
230+
intan_reader = IntanRawIO(filename=file_path)
231+
intan_reader.parse_header()
221232

222-
class TestIntanDigitalDemultiplexShape(unittest.TestCase):
223-
"""Regression coverage for https://github.com/NeuralEnsemble/python-neo/issues/1853."""
224-
225-
def test_demultiplex_handles_packed_digital_buffer(self):
226-
# Digital streams pack all channels into one uint16 word per timestamp.
227-
n_samples = 8
228-
packed = np.array([0, 1, 16, 17, 0, 16, 1, 17], dtype=np.uint16).reshape(n_samples, 1)
229-
expected_bit0 = np.array([0, 1, 0, 1, 0, 0, 1, 1], dtype=np.uint16)
230-
expected_bit4 = np.array([0, 0, 1, 1, 0, 1, 0, 1], dtype=np.uint16)
231-
232-
reader = IntanRawIO.__new__(IntanRawIO)
233-
reader.native_channel_order = {"DIN-00": 0, "DIN-04": 4}
233+
signal_streams = intan_reader.header["signal_streams"]
234+
signal_channels = intan_reader.header["signal_channels"]
235+
stream_names = signal_streams["name"].tolist()
236+
stream_ids = signal_streams["id"].tolist()
234237

235-
chunk = reader._demultiplex_digital_data(packed, ["DIN-00", "DIN-04"], 0, n_samples)
238+
amplifier_stream_index = stream_names.index("RHD2000 amplifier channel")
239+
expected_num_samples = intan_reader.get_signal_size(
240+
block_index=0, seg_index=0, stream_index=amplifier_stream_index
241+
)
236242

237-
self.assertEqual(chunk.shape, (n_samples, 2))
238-
np.testing.assert_array_equal(chunk[:, 0], expected_bit0)
239-
np.testing.assert_array_equal(chunk[:, 1], expected_bit4)
243+
folder_path = file_path.parent
244+
digital_stream_to_raw_file = {
245+
"USB board digital input channel": folder_path / "digitalin.dat",
246+
"USB board digital output channel": folder_path / "digitalout.dat",
247+
}
248+
for stream_name, raw_file_path in digital_stream_to_raw_file.items():
249+
stream_index = stream_names.index(stream_name)
250+
stream_id = stream_ids[stream_index]
251+
channel_ids = [channel["name"] for channel in signal_channels if channel["stream_id"] == stream_id]
252+
assert len(channel_ids) == 16
253+
254+
# The packed word holds all channels, so the stream reports the full sample count.
255+
# Before the fix it was divided by the channel count and get_analogsignal_chunk crashed.
256+
num_samples = intan_reader.get_signal_size(block_index=0, seg_index=0, stream_index=stream_index)
257+
assert num_samples == expected_num_samples
258+
259+
chunk = intan_reader.get_analogsignal_chunk(stream_index=stream_index, channel_ids=channel_ids)
260+
assert chunk.shape == (expected_num_samples, len(channel_ids))
261+
262+
# Each channel is one bit of the packed word; demultiplex the raw file and compare.
263+
# Intan RHD Application Note: Data File Formats (digitalin.dat / digitalout.dat):
264+
# "All 16 digital inputs are encoded bit-by-bit in each 16-bit word. For example, if
265+
# digital inputs 0, 4, and 5 are high and the rest low, the uint16 value for this sample
266+
# time will be 2^0 + 2^4 + 2^5 = 1 + 16 + 32 = 49." The note isolates a channel with the
267+
# MATLAB recipe "digital_input_ch = (bitand(digital_word, 2^ch) > 0)", which is what the
268+
# bitwise_and below reproduces (2^ch -> 1 << bit_position).
269+
packed_words = np.fromfile(raw_file_path, dtype=np.uint16)
270+
for channel_index, channel_id in enumerate(channel_ids):
271+
bit_position = int(channel_id.split("-")[-1])
272+
expected = (np.bitwise_and(packed_words, np.uint16(1 << bit_position)) > 0).astype(np.uint16)
273+
np.testing.assert_array_equal(chunk[:, channel_index], expected)
240274

241275

242276
if __name__ == "__main__":

0 commit comments

Comments
 (0)