@@ -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
242276if __name__ == "__main__" :
0 commit comments