Skip to content

Commit 7244efc

Browse files
authored
Merge pull request #1477 from h-mayorquin/add_annotations_to_intan
Add annotations to intan
2 parents 5c54fd9 + a7f191c commit 7244efc

File tree

2 files changed

+145
-39
lines changed

2 files changed

+145
-39
lines changed

neo/rawio/intanrawio.py

Lines changed: 118 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
99
RHS supported version 1.0
1010
RHD 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
1314
See:
1415
* http://intantech.com/files/Intan_RHD2000_data_file_formats.pdf
@@ -19,7 +20,7 @@
1920
"""
2021

2122
from pathlib import Path
22-
from packaging.version import Version as V
23+
from packaging.version import Version
2324
import warnings
2425

2526
import numpy as np
@@ -31,7 +32,6 @@
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

Comments
 (0)