88
99RHS supported version 1.0
1010RHD supported version 1.0 1.1 1.2 1.3 2.0 3.0, 3.1
11- RHD headerless binary support 3.1
11+ RHD headerless binary support 3.x
12+ RHS headerless binary support 3.x
1213
1314See:
1415 * http://intantech.com/files/Intan_RHD2000_data_file_formats.pdf
1920"""
2021
2122from pathlib import Path
22- from packaging .version import Version as V
23+ from packaging .version import Version
2324import warnings
2425
2526import numpy as np
3132 _signal_stream_dtype ,
3233 _spike_channel_dtype ,
3334 _event_channel_dtype ,
34- _common_sig_characteristics ,
3535)
3636
3737
@@ -42,18 +42,22 @@ class IntanRawIO(BaseRawIO):
4242 Parameters
4343 ----------
4444 filename: str, default: ''
45- name of the 'rhd' or 'rhs' data file
45+ name of the 'rhd' or 'rhs' data/header file
4646 ignore_integrity_checks: bool, default: False
4747 If True, data that violates integrity assumptions will be loaded. At the moment the only integrity
4848 check we perform is that timestamps are continuous. Setting this to True will ignore this check and set
4949 the attribute `discontinuous_timestamps` to True if the timestamps are not continous. This attribute can be checked
5050 after parsing the header to see if the timestamps are continuous or not.
5151 Notes
5252 -----
53- * Intan reader can handle two file formats 'rhd' and 'rhs'. It will automatically
53+ * The Intan reader can handle two file formats 'rhd' and 'rhs'. It will automatically
5454 check for the file extension and will gather the header information based on the
55- extension. Additionally it functions with RHS v 1.0 and RHD 1.0, 1.1, 1.2, 1.3, 2.0,
56- 3.0, and 3.1 files.
55+ extension. Additionally it functions with RHS v 1.0 and v 3.x and RHD 1.0, 1.1, 1.2, 1.3, 2.0,
56+ 3.x files.
57+
58+ * The Intan reader can also handle the headerless binary formats 'one-file-per-signal' and
59+ 'one-file-per-channel' which have a header file called 'info.rhd' or 'info.rhs' and a series
60+ of binary files with the '.dat' suffix
5761
5862 * The reader can handle three file formats 'header-attached', 'one-file-per-signal' and
5963 'one-file-per-channel'.
@@ -68,8 +72,19 @@ class IntanRawIO(BaseRawIO):
6872 4: 'USB board digital input channel',
6973 5: 'USB board digital output channel'
7074
75+ And for RHS:
76+
77+ 0: 'RHS2000 amplfier channel'
78+ 3: 'USB board ADC input channel',
79+ 4: 'USB board ADC output channel',
80+ 5: 'USB board digital input channel',
81+ 6: 'USB board digital output channel',
82+ 10: 'DC Amplifier channel',
83+ 11: 'Stim channel',
84+
7185 * For the "header-attached" and "one-file-per-signal" formats, the structure of the digital input and output channels is
72- one long vector, which must be post-processed to extract individual digital channel information. See the intantech website for more information on performing this post-processing.
86+ one long vector, which must be post-processed to extract individual digital channel information.
87+ See the intantech website for more information on performing this post-processing.
7388
7489 Examples
7590 --------
@@ -104,6 +119,7 @@ def _parse_header(self):
104119 if not filename .exists () or not filename .is_file ():
105120 raise FileNotFoundError (f"{ filename } does not exist" )
106121
122+ # see comment below for RHD which explains the division between file types
107123 if self .filename .endswith (".rhs" ):
108124 if filename .name == "info.rhs" :
109125 if any ((filename .parent / file ).exists () for file in one_file_per_signal_filenames_rhs ):
@@ -117,7 +133,7 @@ def _parse_header(self):
117133
118134 (
119135 self ._global_info ,
120- self ._ordered_channels ,
136+ self ._ordered_channel_info ,
121137 data_dtype ,
122138 header_size ,
123139 self ._block_size ,
@@ -143,7 +159,7 @@ def _parse_header(self):
143159
144160 (
145161 self ._global_info ,
146- self ._ordered_channels ,
162+ self ._ordered_channel_info ,
147163 data_dtype ,
148164 header_size ,
149165 self ._block_size ,
@@ -206,7 +222,7 @@ def _parse_header(self):
206222
207223 # signals
208224 signal_channels = []
209- for c , chan_info in enumerate (self ._ordered_channels ):
225+ for c , chan_info in enumerate (self ._ordered_channel_info ):
210226 name = chan_info ["custom_channel_name" ]
211227 channel_id = chan_info ["native_channel_name" ]
212228 sig_dtype = chan_info ["dtype" ]
@@ -251,6 +267,7 @@ def _parse_header(self):
251267 # are in a list we just take the first channel in each list of channels
252268 else :
253269 self ._max_sigs_length = max ([raw_data [0 ].size for raw_data in self ._raw_data .values ()])
270+
254271 # No events
255272 event_channels = []
256273 event_channels = np .array (event_channels , dtype = _event_channel_dtype )
@@ -270,6 +287,61 @@ def _parse_header(self):
270287
271288 self ._generate_minimal_annotations ()
272289
290+ bl_annotations = self .raw_annotations ["blocks" ][0 ]
291+ seg_annotations = bl_annotations ["segments" ][0 ]
292+
293+ for signal_annotation in seg_annotations ["signals" ]:
294+ # Add global annotations
295+ signal_annotation ["intan_version" ] = (
296+ f"{ self ._global_info ['major_version' ]} ." f"{ self ._global_info ['minor_version' ]} "
297+ )
298+ global_keys_to_skip = [
299+ "major_version" ,
300+ "minor_version" ,
301+ "sampling_rate" ,
302+ "magic_number" ,
303+ "reference_channel" ,
304+ ]
305+ global_keys_to_annotate = set (self ._global_info .keys ()) - set (global_keys_to_skip )
306+ for key in global_keys_to_annotate :
307+ signal_annotation [key ] = self ._global_info [key ]
308+
309+ reference_channel = self ._global_info .get ("reference_channel" , None )
310+ # Following the pdf specification
311+ reference_channel = "hardware" if reference_channel == "n/a" else reference_channel
312+
313+ # Add channel annotations
314+ array_annotations = signal_annotation ["__array_annotations__" ]
315+ channel_ids = array_annotations ["channel_ids" ]
316+
317+ # TODO refactor ordered channel dict to make this easier
318+ # Use this to find which elements of the ordered channels correspond to the current signal
319+ signal_type = int (signal_annotation ["stream_id" ])
320+ channel_info = next ((info for info in self ._ordered_channel_info if info ["signal_type" ] == signal_type ))
321+ channel_keys_to_skip = [
322+ "signal_type" ,
323+ "custom_channel_name" ,
324+ "native_channel_name" ,
325+ "gain" ,
326+ "offset" ,
327+ "channel_enabled" ,
328+ "dtype" ,
329+ "units" ,
330+ "sampling_rate" ,
331+ ]
332+
333+ channel_keys_to_annotate = set (channel_info .keys ()) - set (channel_keys_to_skip )
334+ properties_dict = {key : [] for key in channel_keys_to_annotate }
335+ for channel_id in channel_ids :
336+ matching_info = next (
337+ info for info in self ._ordered_channel_info if info ["native_channel_name" ] == channel_id
338+ )
339+ for key in channel_keys_to_annotate :
340+ properties_dict [key ].append (matching_info [key ])
341+
342+ for key in channel_keys_to_annotate :
343+ array_annotations [key ] = properties_dict [key ]
344+
273345 def _segment_t_start (self , block_index , seg_index ):
274346 return 0.0
275347
@@ -517,7 +589,7 @@ def read_rhs(filename, file_format: str):
517589 sr = global_info ["sampling_rate" ]
518590
519591 # construct dtype by re-ordering channels by types
520- ordered_channels = []
592+ ordered_channel_info = []
521593 if file_format == "header-attached" :
522594 data_dtype = [("timestamp" , "int32" , BLOCK_SIZE )]
523595 else :
@@ -537,7 +609,7 @@ def read_rhs(filename, file_format: str):
537609 chan_info ["dtype" ] = "uint16"
538610 else :
539611 chan_info ["dtype" ] = "int16"
540- ordered_channels .append (chan_info )
612+ ordered_channel_info .append (chan_info )
541613 if file_format == "header-attached" :
542614 name = chan_info ["native_channel_name" ]
543615 data_dtype += [(name , "uint16" , BLOCK_SIZE )]
@@ -557,7 +629,7 @@ def read_rhs(filename, file_format: str):
557629 chan_info_dc ["offset" ] = - 512 * 19.23
558630 chan_info_dc ["signal_type" ] = 10 # put it in another group
559631 chan_info_dc ["dtype" ] = "uint16"
560- ordered_channels .append (chan_info_dc )
632+ ordered_channel_info .append (chan_info_dc )
561633 if file_format == "header-attached" :
562634 data_dtype += [(name + "_DC" , "uint16" , BLOCK_SIZE )]
563635 else :
@@ -579,7 +651,7 @@ def read_rhs(filename, file_format: str):
579651 chan_info_stim ["offset" ] = 0.0
580652 chan_info_stim ["signal_type" ] = 11 # put it in another group
581653 chan_info_stim ["dtype" ] = "uint16"
582- ordered_channels .append (chan_info_stim )
654+ ordered_channel_info .append (chan_info_stim )
583655 if file_format == "header-attached" :
584656 data_dtype += [(name + "_STIM" , "uint16" , BLOCK_SIZE )]
585657 else :
@@ -598,7 +670,7 @@ def read_rhs(filename, file_format: str):
598670 chan_info ["gain" ] = 0.0003125
599671 chan_info ["offset" ] = - 32768 * 0.0003125
600672 chan_info ["dtype" ] = "uint16"
601- ordered_channels .append (chan_info )
673+ ordered_channel_info .append (chan_info )
602674 if file_format == "header-attached" :
603675 name = chan_info ["native_channel_name" ]
604676 data_dtype += [(name , "uint16" , BLOCK_SIZE )]
@@ -622,7 +694,7 @@ def read_rhs(filename, file_format: str):
622694 chan_info ["gain" ] = 1.0
623695 chan_info ["offset" ] = 0.0
624696 chan_info ["dtype" ] = "uint16"
625- ordered_channels .append (chan_info )
697+ ordered_channel_info .append (chan_info )
626698 if file_format == "header-attached" :
627699 data_dtype += [(name , "uint16" , BLOCK_SIZE )]
628700 else :
@@ -635,12 +707,16 @@ def read_rhs(filename, file_format: str):
635707 chan_info ["gain" ] = 1.0
636708 chan_info ["offset" ] = 0.0
637709 chan_info ["dtype" ] = "uint16"
638- ordered_channels .append (chan_info )
710+ ordered_channel_info .append (chan_info )
639711 data_dtype [sig_type ] = "uint16"
640712
641- if global_info ["notch_filter_mode" ] == 2 and global_info ["major_version" ] >= V ("3.0" ):
713+ # per discussion with Intan developers before version 3 of their software the 'notch_filter_mode'
714+ # was a request for postprocessing to be done in one of their scripts. From version 3+ the notch
715+ # filter is now applied to the data in realtime and only the post notched amplifier data is
716+ # saved.
717+ if global_info ["notch_filter_mode" ] == 2 and global_info ["major_version" ] >= Version ("3.0" ):
642718 global_info ["notch_filter" ] = "60Hz"
643- elif global_info ["notch_filter_mode" ] == 1 and global_info ["major_version" ] >= V ("3.0" ):
719+ elif global_info ["notch_filter_mode" ] == 1 and global_info ["major_version" ] >= Version ("3.0" ):
644720 global_info ["notch_filter" ] = "50Hz"
645721 else :
646722 global_info ["notch_filter" ] = False
@@ -650,7 +726,7 @@ def read_rhs(filename, file_format: str):
650726 data_dtype = {k : v for (k , v ) in data_dtype .items () if len (v ) > 0 }
651727 channel_number_dict = {k : v for (k , v ) in channel_number_dict .items () if v > 0 }
652728
653- return global_info , ordered_channels , data_dtype , header_size , BLOCK_SIZE , channel_number_dict
729+ return global_info , ordered_channel_info , data_dtype , header_size , BLOCK_SIZE , channel_number_dict
654730
655731
656732###############
@@ -745,22 +821,22 @@ def read_rhd(filename, file_format: str):
745821
746822 global_info = read_variable_header (f , rhd_global_header_base )
747823
748- version = V (f"{ global_info ['major_version' ]} .{ global_info ['minor_version' ]} " )
824+ version = Version (f"{ global_info ['major_version' ]} .{ global_info ['minor_version' ]} " )
749825
750826 # the header size depends on the version :-(
751827 header = list (rhd_global_header_part1 ) # make a copy
752828
753- if version >= V ("1.1" ):
829+ if version >= Version ("1.1" ):
754830 header = header + rhd_global_header_v11
755831 else :
756832 global_info ["num_temp_sensor_channels" ] = 0
757833
758- if version >= V ("1.3" ):
834+ if version >= Version ("1.3" ):
759835 header = header + rhd_global_header_v13
760836 else :
761837 global_info ["eval_board_mode" ] = 0
762838
763- if version >= V ("2.0" ):
839+ if version >= Version ("2.0" ):
764840 header = header + rhd_global_header_v20
765841 else :
766842 global_info ["reference_channel" ] = ""
@@ -789,14 +865,14 @@ def read_rhd(filename, file_format: str):
789865 sr = global_info ["sampling_rate" ]
790866
791867 # construct the data block dtype and reorder channels
792- if version >= V ("2.0" ):
868+ if version >= Version ("2.0" ):
793869 BLOCK_SIZE = 128
794870 else :
795871 BLOCK_SIZE = 60 # 256 channels
796872
797- ordered_channels = []
873+ ordered_channel_info = []
798874
799- if version >= V ("1.2" ):
875+ if version >= Version ("1.2" ):
800876 if file_format == "header-attached" :
801877 data_dtype = [("timestamp" , "int32" , BLOCK_SIZE )]
802878 else :
@@ -820,7 +896,7 @@ def read_rhd(filename, file_format: str):
820896 else :
821897 chan_info ["offset" ] = 0.0
822898 chan_info ["dtype" ] = "int16"
823- ordered_channels .append (chan_info )
899+ ordered_channel_info .append (chan_info )
824900
825901 if file_format == "header-attached" :
826902 name = chan_info ["native_channel_name" ]
@@ -835,7 +911,7 @@ def read_rhd(filename, file_format: str):
835911 chan_info ["gain" ] = 0.0000374
836912 chan_info ["offset" ] = 0.0
837913 chan_info ["dtype" ] = "uint16"
838- ordered_channels .append (chan_info )
914+ ordered_channel_info .append (chan_info )
839915 if file_format == "header-attached" :
840916 name = chan_info ["native_channel_name" ]
841917 data_dtype += [(name , "uint16" , BLOCK_SIZE // 4 )]
@@ -849,7 +925,7 @@ def read_rhd(filename, file_format: str):
849925 chan_info ["gain" ] = 0.0000748
850926 chan_info ["offset" ] = 0.0
851927 chan_info ["dtype" ] = "uint16"
852- ordered_channels .append (chan_info )
928+ ordered_channel_info .append (chan_info )
853929 if file_format == "header-attached" :
854930 name = chan_info ["native_channel_name" ]
855931 data_dtype += [(name , "uint16" )]
@@ -865,7 +941,7 @@ def read_rhd(filename, file_format: str):
865941 chan_info ["gain" ] = 0.001
866942 chan_info ["offset" ] = 0.0
867943 chan_info ["dtype" ] = "int16"
868- ordered_channels .append (chan_info )
944+ ordered_channel_info .append (chan_info )
869945 data_dtype += [(name , "int16" )]
870946
871947 # 3: USB board ADC input channel
@@ -882,7 +958,7 @@ def read_rhd(filename, file_format: str):
882958 chan_info ["gain" ] = 0.0003125
883959 chan_info ["offset" ] = - 32768 * 0.0003125
884960 chan_info ["dtype" ] = "uint16"
885- ordered_channels .append (chan_info )
961+ ordered_channel_info .append (chan_info )
886962 if file_format == "header-attached" :
887963 name = chan_info ["native_channel_name" ]
888964 data_dtype += [(name , "uint16" , BLOCK_SIZE )]
@@ -906,7 +982,7 @@ def read_rhd(filename, file_format: str):
906982 chan_info ["gain" ] = 1.0
907983 chan_info ["offset" ] = 0.0
908984 chan_info ["dtype" ] = "uint16"
909- ordered_channels .append (chan_info )
985+ ordered_channel_info .append (chan_info )
910986 if file_format == "header-attached" :
911987 data_dtype += [(name , "uint16" , BLOCK_SIZE )]
912988 else :
@@ -918,12 +994,16 @@ def read_rhd(filename, file_format: str):
918994 chan_info ["gain" ] = 1.0
919995 chan_info ["offset" ] = 0.0
920996 chan_info ["dtype" ] = "uint16"
921- ordered_channels .append (chan_info )
997+ ordered_channel_info .append (chan_info )
922998 data_dtype [sig_type ] = "uint16"
923999
924- if global_info ["notch_filter_mode" ] == 2 and version >= V ("3.0" ):
1000+ # per discussion with Intan developers before version 3 of their software the 'notch_filter_mode'
1001+ # was a request for postprocessing to be done in one of their scripts. From version 3+ the notch
1002+ # filter is now applied to the data in realtime and only the post notched amplifier data is
1003+ # saved.
1004+ if global_info ["notch_filter_mode" ] == 2 and version >= Version ("3.0" ):
9251005 global_info ["notch_filter" ] = "60Hz"
926- elif global_info ["notch_filter_mode" ] == 1 and version >= V ("3.0" ):
1006+ elif global_info ["notch_filter_mode" ] == 1 and version >= Version ("3.0" ):
9271007 global_info ["notch_filter" ] = "50Hz"
9281008 else :
9291009 global_info ["notch_filter" ] = False
@@ -933,7 +1013,7 @@ def read_rhd(filename, file_format: str):
9331013 data_dtype = {k : v for (k , v ) in data_dtype .items () if len (v ) > 0 }
9341014 channel_number_dict = {k : v for (k , v ) in channel_number_dict .items () if v > 0 }
9351015
936- return global_info , ordered_channels , data_dtype , header_size , BLOCK_SIZE , channel_number_dict
1016+ return global_info , ordered_channel_info , data_dtype , header_size , BLOCK_SIZE , channel_number_dict
9371017
9381018
9391019##########################################################################
0 commit comments