@@ -63,14 +63,12 @@ class IntanRawIO(BaseRawIO):
6363 'one-file-per-channel' which have a header file called 'info.rhd' or 'info.rhs' and a series
6464 of binary files with the '.dat' suffix
6565
66- * The reader can handle three file formats 'header-attached', 'one-file-per-signal' and
67- 'one-file-per-channel'.
68-
69- * Intan files contain amplifier channels labeled 'A', 'B' 'C' or 'D'
70- depending on the port in which they were recorded along with the following
66+ * Intan files contain amplifier channels labeled 'A', 'B' 'C' or 'D' for the 512 recorder
67+ or 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H' for the 1024 recorder system
68+ depending on the port in which they were recorded along (stored in stream_id '0') with the following
7169 additional streams.
7270
73- 0: 'RHD2000' amplifier channel
71+ 0: 'RHD2000 amplifier channel'
7472 1: 'RHD2000 auxiliary input channel',
7573 2: 'RHD2000 supply voltage channel',
7674 3: 'USB board ADC input channel',
@@ -87,9 +85,11 @@ class IntanRawIO(BaseRawIO):
8785 10: 'DC Amplifier channel',
8886 11: 'Stim channel',
8987
90- * For the "header-attached" and "one-file-per-signal" formats, the structure of the digital input and output channels is
91- one long vector, which must be post-processed to extract individual digital channel information.
92- See the intantech website for more information on performing this post-processing.
88+ * We currently implement digital data demultiplexing so that if digital streams are requested they are
89+ returned as arrays of 1s and 0s.
90+
91+ * We also do stim data decoding which returns the stim data as an int16 of appropriate magnitude. Please
92+ use `rescale_signal_raw_to_float` to obtain stim data in amperes.
9393
9494
9595 Examples
@@ -442,6 +442,7 @@ def _get_analogsignal_chunk_header_attached(self, i_start, i_stop, stream_index,
442442
443443 stream_name = self .header ["signal_streams" ][stream_index ]["name" ][:]
444444 stream_is_digital = stream_name in digital_stream_names
445+ stream_is_stim = stream_name == "Stim channel"
445446
446447 field_name = stream_name if stream_is_digital else channel_ids [0 ]
447448
@@ -460,17 +461,22 @@ def _get_analogsignal_chunk_header_attached(self, i_start, i_stop, stream_index,
460461 sl0 = i_start % block_size
461462 sl1 = sl0 + (i_stop - i_start )
462463
463- # For all streams raw_data is a structured memmap with a field for each channel_id
464464 if not stream_is_digital :
465+ # For all streams raw_data is a structured memmap with a field for each channel_id
465466 sigs_chunk = np .zeros ((i_stop - i_start , len (channel_ids )), dtype = dtype )
466-
467467 for chunk_index , channel_id in enumerate (channel_ids ):
468468 data_chan = self ._raw_data [channel_id ]
469+
469470 if multiple_samples_per_block :
470471 sigs_chunk [:, chunk_index ] = data_chan [block_start :block_stop ].flatten ()[sl0 :sl1 ]
471472 else :
472473 sigs_chunk [:, chunk_index ] = data_chan [i_start :i_stop ]
473- else : # For digital data the channels come interleaved in a single field so we need to demultiplex
474+
475+ if stream_is_stim :
476+ sigs_chunk = self ._decode_current_from_stim_data (sigs_chunk , 0 , sigs_chunk .shape [0 ])
477+
478+ else :
479+ # For digital data the channels come interleaved in a single field so we need to demultiplex
474480 digital_raw_data = self ._raw_data [field_name ].flatten ()
475481 sigs_chunk = self ._demultiplex_digital_data (digital_raw_data , channel_ids , i_start , i_stop )
476482 return sigs_chunk
@@ -479,6 +485,8 @@ def _get_analogsignal_chunk_one_file_per_channel(self, i_start, i_stop, stream_i
479485
480486 stream_name = self .header ["signal_streams" ][stream_index ]["name" ][:]
481487 signal_data_memmap_list = self ._raw_data [stream_name ]
488+ stream_is_stim = stream_name == "Stim channel"
489+
482490 channel_indexes_are_slice = isinstance (channel_indexes , slice )
483491 if channel_indexes_are_slice :
484492 num_channels = len (signal_data_memmap_list )
@@ -496,6 +504,10 @@ def _get_analogsignal_chunk_one_file_per_channel(self, i_start, i_stop, stream_i
496504 channel_memmap = signal_data_memmap_list [channel_index ]
497505 sigs_chunk [:, chunk_index ] = channel_memmap [i_start :i_stop ]
498506
507+ # If this is stim data, we need to decode the current values
508+ if stream_is_stim :
509+ sigs_chunk = self ._decode_current_from_stim_data (sigs_chunk , 0 , sigs_chunk .shape [0 ])
510+
499511 return sigs_chunk
500512
501513 def _get_analogsignal_chunk_one_file_per_signal (self , i_start , i_stop , stream_index , channel_indexes ):
@@ -504,21 +516,57 @@ def _get_analogsignal_chunk_one_file_per_signal(self, i_start, i_stop, stream_in
504516 raw_data = self ._raw_data [stream_name ]
505517
506518 stream_is_digital = stream_name in digital_stream_names
519+ stream_is_stim = stream_name == "Stim channel"
520+
507521 if stream_is_digital :
508522 stream_id = self .header ["signal_streams" ][stream_index ]["id" ]
509523 mask = self .header ["signal_channels" ]["stream_id" ] == stream_id
510524 signal_channels = self .header ["signal_channels" ][mask ]
511525 channel_ids = signal_channels ["id" ][channel_indexes ]
512526
513527 output = self ._demultiplex_digital_data (raw_data , channel_ids , i_start , i_stop )
514-
528+ elif stream_is_stim :
529+ output = self ._decode_current_from_stim_data (raw_data , i_start , i_stop )
530+ output = output [:, channel_indexes ]
515531 else :
516532 output = raw_data [i_start :i_stop , channel_indexes ]
517533
518534 return output
519535
520536 def _demultiplex_digital_data (self , raw_digital_data , channel_ids , i_start , i_stop ):
537+ """
538+ Demultiplex digital data by extracting individual channel values from packed 16-bit format.
539+
540+ According to the Intan format, digital input/output data is stored with all 16 channels
541+ encoded bit-by-bit in each 16-bit word. This method extracts the specified digital channels
542+ from the packed format into separate uint16 arrays of 0 and 1.
543+
544+ Parameters
545+ ----------
546+ raw_digital_data : ndarray
547+ Raw digital data in packed 16-bit format where each bit represents a different channel.
548+ channel_ids : list or array
549+ List of channel identifiers to extract. Each channel_id must correspond to a digital
550+ input or output channel.
551+ i_start : int
552+ Starting sample index for demultiplexing.
553+ i_stop : int
554+ Ending sample index for demultiplexing (exclusive).
555+
556+ Returns
557+ -------
558+ ndarray
559+ Demultiplexed digital data with shape (i_stop-i_start, len(channel_ids)),
560+ containing 0 or 1 values for each requested channel.
521561
562+ Notes
563+ -----
564+ In the Intan format, digital channels are packed into 16-bit words where each bit position
565+ corresponds to a specific channel number. For example, with digital inputs 0, 4, and 5
566+ set high and the rest low, the 16-bit word would be 2^0 + 2^4 + 2^5 = 1 + 16 + 32 = 49.
567+
568+ The native_order property for each channel corresponds to its bit position in the packed word.
569+ """
522570 dtype = np .uint16 # We fix this to match the memmap dtype
523571 output = np .zeros ((i_stop - i_start , len (channel_ids )), dtype = dtype )
524572
@@ -530,6 +578,65 @@ def _demultiplex_digital_data(self, raw_digital_data, channel_ids, i_start, i_st
530578
531579 return output
532580
581+ def _decode_current_from_stim_data (self , raw_stim_data , i_start , i_stop ):
582+ """
583+ Demultiplex stimulation data by extracting current values from packed 16-bit format.
584+
585+ According to the Intan RHS data format, stimulation current is stored in the lower 9 bits
586+ of each 16-bit word: 8 bits for magnitude and 1 bit for sign. The upper bits contain
587+ flags for compliance limit, charge recovery, and amplifier settle.
588+
589+ Parameters
590+ ----------
591+ raw_stim_data : ndarray
592+ Raw stimulation data in packed 16-bit format.
593+ i_start : int
594+ Starting sample index for demultiplexing.
595+ i_stop : int
596+ Ending sample index for demultiplexing (exclusive).
597+
598+ Returns
599+ -------
600+ ndarray
601+ Demultiplexed stimulation current values in amperes, preserving the original
602+ array dimensions. The output values need to be multiplied by the stim_step_size
603+ parameter (from header) to obtain the actual current in amperes.
604+
605+ Notes
606+ -----
607+ Bit structure of each 16-bit stimulation word:
608+ - Bits 0-7: Current magnitude
609+ - Bit 8: Sign bit (1 = negative current)
610+ - Bits 9-13: Unused (always zero)
611+ - Bit 14: Amplifier settle flag (1 = activated)
612+ - Bit 15: Charge recovery flag (1 = activated)
613+ - Bit 16 (MSB): Compliance limit flag (1 = limit reached)
614+
615+ The actual current value in amperes is obtained by multiplying the
616+ output by the 'stim_step_size' parameter from the file header. These scaled values can be
617+ obtained with the `rescale_signal_raw_to_float` function.
618+ """
619+ # Get the relevant portion of the data
620+ data = raw_stim_data [i_start :i_stop ]
621+
622+ # Extract current value (bits 0-8)
623+ magnitude = np .bitwise_and (data , 0xFF )
624+ sign_bit = np .bitwise_and (np .right_shift (data , 8 ), 0x01 ) # Shift right by 8 bits to get the sign bit
625+
626+ # Apply sign to current values
627+ # We need to cast to int16 to handle negative values correctly
628+ # The max value of 8 bits is 255 so the casting is safe as there are non-negative values
629+ magnitude = magnitude .astype (np .int16 )
630+ current = np .where (sign_bit == 1 , - magnitude , magnitude )
631+
632+ # Note: If needed, other flag bits could be extracted as follows:
633+ # compliance_flag = np.bitwise_and(np.right_shift(data, 15), 0x01).astype(bool) # Bit 16 (MSB)
634+ # charge_recovery_flag = np.bitwise_and(np.right_shift(data, 14), 0x01).astype(bool) # Bit 15
635+ # amp_settle_flag = np.bitwise_and(np.right_shift(data, 13), 0x01).astype(bool) # Bit 14
636+ # These could be returned as a structured array or dictionary if needed
637+
638+ return current
639+
533640 def get_intan_timestamps (self , i_start = None , i_stop = None ):
534641 """
535642 Retrieves the sample indices from the Intan raw data within a specified range.
@@ -793,8 +900,18 @@ def read_rhs(filename, file_format: str):
793900 channel_number_dict = {name : len (stream_name_to_channel_info_list [name ]) for name in names_to_count }
794901
795902 # Both DC Amplifier and Stim streams have the same number of channels as the amplifier stream
903+ # if using the `header-attached` or `one-file-per-signal` formats
904+ # the amplifier data is stored in the same place in the header so no matter what the DC amp
905+ # uses the same info as the RHS amp.
796906 channel_number_dict ["DC Amplifier channel" ] = channel_number_dict ["RHS2000 amplifier channel" ]
797- channel_number_dict ["Stim channel" ] = channel_number_dict ["RHS2000 amplifier channel" ]
907+ if file_format != "one-file-per-channel" :
908+ channel_number_dict ["Stim channel" ] = channel_number_dict ["RHS2000 amplifier channel" ]
909+ raw_file_paths_dict = create_one_file_per_signal_dict_rhs (dirname = filename .parent )
910+ else :
911+ raw_file_paths_dict = create_one_file_per_channel_dict_rhs (dirname = filename .parent )
912+ channel_number_dict ["Stim channel" ] = len (raw_file_paths_dict ["Stim channel" ])
913+ # but the user can shut off the normal amplifier and only save dc amplifier
914+ channel_number_dict ["RHS2000 amplifier channel" ] = len (raw_file_paths_dict ["RHS2000 amplifier channel" ])
798915
799916 header_size = f .tell ()
800917
@@ -808,24 +925,25 @@ def read_rhs(filename, file_format: str):
808925 memmap_data_dtype ["timestamp" ] = "int32"
809926 channel_number_dict ["timestamp" ] = 1
810927
811- for chan_info in stream_name_to_channel_info_list ["RHS2000 amplifier channel" ]:
812- chan_info ["sampling_rate" ] = sr
813- chan_info ["units" ] = "uV"
814- chan_info ["gain" ] = 0.195
815- if file_format == "header-attached" :
816- chan_info ["offset" ] = - 32768 * 0.195
817- else :
818- chan_info ["offset" ] = 0.0
819- if file_format == "header-attached" :
820- chan_info ["dtype" ] = "uint16"
821- else :
822- chan_info ["dtype" ] = "int16"
823- ordered_channel_info .append (chan_info )
824- if file_format == "header-attached" :
825- name = chan_info ["native_channel_name" ]
826- memmap_data_dtype += [(name , "uint16" , BLOCK_SIZE )]
827- else :
828- memmap_data_dtype ["RHS2000 amplifier channel" ] = "int16"
928+ if file_format != "one-file-per-channel" or channel_number_dict ["RHS2000 amplifier channel" ] > 0 :
929+ for chan_info in stream_name_to_channel_info_list ["RHS2000 amplifier channel" ]:
930+ chan_info ["sampling_rate" ] = sr
931+ chan_info ["units" ] = "uV"
932+ chan_info ["gain" ] = 0.195
933+ if file_format == "header-attached" :
934+ chan_info ["offset" ] = - 32768 * 0.195
935+ else :
936+ chan_info ["offset" ] = 0.0
937+ if file_format == "header-attached" :
938+ chan_info ["dtype" ] = "uint16"
939+ else :
940+ chan_info ["dtype" ] = "int16"
941+ ordered_channel_info .append (chan_info )
942+ if file_format == "header-attached" :
943+ name = chan_info ["native_channel_name" ]
944+ memmap_data_dtype += [(name , "uint16" , BLOCK_SIZE )]
945+ else :
946+ memmap_data_dtype ["RHS2000 amplifier channel" ] = "int16"
829947
830948 if bool (global_info ["dc_amplifier_data_saved" ]):
831949 # if we have dc amp we need to grab the correct number of channels
@@ -847,27 +965,31 @@ def read_rhs(filename, file_format: str):
847965 memmap_data_dtype ["DC Amplifier channel" ] = "uint16"
848966
849967 # I can't seem to get stim files to generate for one-file-per-channel
850- # so let's skip for now and can be given on request
851- if file_format != "one-file-per-channel" :
852- for chan_info in stream_name_to_channel_info_list ["RHS2000 amplifier channel" ]:
968+ # so ideally at some point we need test data to confirm this is true
969+ # based on what Heberto and I read in the docs
970+ for chan_info in stream_name_to_channel_info_list ["RHS2000 amplifier channel" ]:
971+ # we see which stim were activated
972+ if file_format != "one-file-per-channel" or any (
973+ [chan_info ["native_channel_name" ] in stim_file .stem for stim_file in raw_file_paths_dict ["Stim channel" ]]
974+ ):
853975 chan_info_stim = dict (chan_info )
854976 name = chan_info ["native_channel_name" ]
855977 chan_info_stim ["native_channel_name" ] = name + "_STIM"
856978 chan_info_stim ["sampling_rate" ] = sr
857979 # stim channel are complicated because they are coded
858980 # with bits, they do not fit the gain/offset rawio strategy
859- chan_info_stim ["units" ] = ""
860- chan_info_stim ["gain" ] = 1.0
981+ chan_info_stim ["units" ] = "A" # Amps
982+ chan_info_stim ["gain" ] = global_info [ "stim_step_size" ]
861983 chan_info_stim ["offset" ] = 0.0
862984 chan_info_stim ["signal_type" ] = 11 # put it in another group
863- chan_info_stim ["dtype" ] = "uint16"
985+ chan_info_stim ["dtype" ] = "int16" # this change is due to bit decoding see note below
864986 ordered_channel_info .append (chan_info_stim )
987+ # Note that the data on disk is uint16 but the data is
988+ # then decoded as int16 so the chan_info is int16
865989 if file_format == "header-attached" :
866990 memmap_data_dtype += [(name + "_STIM" , "uint16" , BLOCK_SIZE )]
867991 else :
868992 memmap_data_dtype ["Stim channel" ] = "uint16"
869- else :
870- warnings .warn ("Stim not implemented for `one-file-per-channel` due to lack of test files" )
871993
872994 # No supply or aux for rhs files (ie no stream_id 1 and 2)
873995 # We have an error above that requests test files to help if the spec is changed
0 commit comments