Skip to content

Commit d265dc6

Browse files
authored
Merge pull request #1593 from catalystneuro/spikegadgets
Added support for missing channels in SpikeGadgets
2 parents b2a358a + 4713328 commit d265dc6

File tree

2 files changed

+48
-11
lines changed

2 files changed

+48
-11
lines changed

neo/rawio/spikegadgetsrawio.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def __init__(self, filename="", selected_streams=None):
8181
def _source_name(self):
8282
return self.filename
8383

84-
def _produce_ephys_channel_ids(self, n_total_channels, n_channels_per_chip):
84+
def _produce_ephys_channel_ids(self, n_total_channels, n_channels_per_chip, missing_hw_chans):
8585
"""Compute the channel ID labels for subset of spikegadgets recordings
8686
The ephys channels in the .rec file are stored in the following order:
8787
hwChan ID of channel 0 of first chip, hwChan ID of channel 0 of second chip, ..., hwChan ID of channel 0 of Nth chip,
@@ -94,14 +94,18 @@ def _produce_ephys_channel_ids(self, n_total_channels, n_channels_per_chip):
9494
This doesn't work for all types of spikegadgets
9595
see: https://github.com/NeuralEnsemble/python-neo/issues/1517
9696
97+
If there are any missing hardware channels, they must be specified in missing_hw_chans.
98+
See: https://github.com/NeuralEnsemble/python-neo/issues/1592
9799
"""
98100
ephys_channel_ids_list = []
99-
for hw_channel in range(n_channels_per_chip):
100-
hw_channel_list = [
101-
hw_channel + chip * n_channels_per_chip for chip in range(int(n_total_channels / n_channels_per_chip))
102-
]
103-
ephys_channel_ids_list.append(hw_channel_list)
104-
return [channel for channel_list in ephys_channel_ids_list for channel in channel_list]
101+
for local_hw_channel in range(n_channels_per_chip):
102+
n_chips = int(n_total_channels / n_channels_per_chip)
103+
for chip in range(n_chips):
104+
global_hw_chan = local_hw_channel + chip * n_channels_per_chip
105+
if global_hw_chan in missing_hw_chans:
106+
continue
107+
ephys_channel_ids_list.append(local_hw_channel + chip * n_channels_per_chip)
108+
return ephys_channel_ids_list
105109

106110
def _parse_header(self):
107111
# parse file until "</Configuration>"
@@ -126,7 +130,8 @@ def _parse_header(self):
126130
sconf = root.find("SpikeConfiguration")
127131

128132
self._sampling_rate = float(hconf.attrib["samplingRate"])
129-
num_ephy_channels = int(hconf.attrib["numChannels"])
133+
num_ephy_channels_xml = int(hconf.attrib["numChannels"])
134+
num_ephy_channels = num_ephy_channels_xml
130135

131136
# check for agreement with number of channels in xml
132137
sconf_channels = np.sum([len(x) for x in sconf])
@@ -220,7 +225,9 @@ def _parse_header(self):
220225
# we can only produce these channels for a subset of spikegadgets setup. If this criteria isn't
221226
# true then we should just use the raw_channel_ids and let the end user sort everything out
222227
if num_ephy_channels % num_chan_per_chip == 0:
223-
channel_ids = self._produce_ephys_channel_ids(num_ephy_channels, num_chan_per_chip)
228+
all_hw_chans = [int(schan.attrib["hwChan"]) for trode in sconf for schan in trode]
229+
missing_hw_chans = set(range(num_ephy_channels)) - set(all_hw_chans)
230+
channel_ids = self._produce_ephys_channel_ids(num_ephy_channels_xml, num_chan_per_chip, missing_hw_chans)
224231
raw_channel_ids = False
225232
else:
226233
raw_channel_ids = True

neo/test/rawiotest/test_spikegadgetsrawio.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import unittest
2+
from pathlib import Path
23

34
from neo.rawio.spikegadgetsrawio import SpikeGadgetsRawIO
45
from neo.test.rawiotest.common_rawio_test import BaseTestRawIO
6+
from numpy.testing import assert_array_equal
57

68

79
class TestSpikeGadgetsRawIO(
@@ -16,6 +18,34 @@ class TestSpikeGadgetsRawIO(
1618
"spikegadgets/SpikeGadgets_test_data_2xNpix1.0_20240318_173658.rec",
1719
]
1820

21+
def test_parse_header_missing_channels(self):
1922

20-
if __name__ == "__main__":
21-
unittest.main()
23+
file_path = Path(self.get_local_path("spikegadgets/SL18_D19_S01_F01_BOX_SLP_20230503_112642_stubbed.rec"))
24+
reader = SpikeGadgetsRawIO(filename = file_path)
25+
reader.parse_header()
26+
27+
assert_array_equal(
28+
reader.header['signal_channels']['id'],
29+
[
30+
'ECU_Ain1', 'ECU_Ain2', 'ECU_Ain3', 'ECU_Ain4', 'ECU_Ain5', 'ECU_Ain6',
31+
'ECU_Ain7', 'ECU_Ain8', 'ECU_Aout1', 'ECU_Aout2', 'ECU_Aout3', 'ECU_Aout4', '0',
32+
'32', '96', '160', '192', '224', '1', '33', '65', '97', '161', '193', '225', '2', '34',
33+
'98', '162', '194', '226', '3', '35', '67', '99', '163', '195', '227', '4', '36',
34+
'100', '164', '196', '228', '5', '37', '69', '101', '165', '197', '229', '6', '38',
35+
'102', '166', '198', '230', '7', '39', '71', '103', '167', '199', '231', '8', '40',
36+
'72', '104', '136', '168', '200', '232', '9', '41', '73', '105', '137', '169', '201',
37+
'233', '10', '42', '74', '106', '138', '170', '202', '234', '11', '43', '75', '107',
38+
'139', '171', '203', '235', '12', '44', '76', '108', '140', '172', '204', '236', '13',
39+
'45', '77', '109', '141', '173', '205', '237', '14', '46', '78', '110', '142', '174',
40+
'206', '238', '15', '47', '79', '111', '143', '175', '207', '239', '80', '144', '176',
41+
'208', '240', '17', '49', '81', '145', '177', '209', '241', '82', '146', '178', '210',
42+
'242', '19', '51', '83', '147', '179', '211', '243', '84', '148', '180', '212', '244',
43+
'21', '53', '85', '149', '181', '213', '245', '86', '150', '182', '214', '246', '23',
44+
'55', '87', '151', '183', '215', '247', '24', '56', '88', '152', '184', '216', '248',
45+
'25', '57', '89', '121', '153', '185', '217', '249', '26', '58', '90', '154', '186',
46+
'218', '250', '27', '59', '91', '123', '155', '187', '219', '251', '28', '60', '92',
47+
'156', '188', '220', '252', '29', '61', '93', '125', '157', '189', '221', '253', '30',
48+
'62', '94', '158', '190', '222', '254', '31', '63', '95', '127', '159', '191', '223',
49+
'255',
50+
]
51+
)

0 commit comments

Comments
 (0)