diff --git a/neo/rawio/spikegadgetsrawio.py b/neo/rawio/spikegadgetsrawio.py index c9fa909e6..f15aafaff 100644 --- a/neo/rawio/spikegadgetsrawio.py +++ b/neo/rawio/spikegadgetsrawio.py @@ -81,7 +81,7 @@ def __init__(self, filename="", selected_streams=None): def _source_name(self): return self.filename - def _produce_ephys_channel_ids(self, n_total_channels, n_channels_per_chip): + def _produce_ephys_channel_ids(self, n_total_channels, n_channels_per_chip, missing_hw_chans): """Compute the channel ID labels for subset of spikegadgets recordings The ephys channels in the .rec file are stored in the following order: 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): This doesn't work for all types of spikegadgets see: https://github.com/NeuralEnsemble/python-neo/issues/1517 + If there are any missing hardware channels, they must be specified in missing_hw_chans. + See: https://github.com/NeuralEnsemble/python-neo/issues/1592 """ ephys_channel_ids_list = [] - for hw_channel in range(n_channels_per_chip): - hw_channel_list = [ - hw_channel + chip * n_channels_per_chip for chip in range(int(n_total_channels / n_channels_per_chip)) - ] - ephys_channel_ids_list.append(hw_channel_list) - return [channel for channel_list in ephys_channel_ids_list for channel in channel_list] + for local_hw_channel in range(n_channels_per_chip): + n_chips = int(n_total_channels / n_channels_per_chip) + for chip in range(n_chips): + global_hw_chan = local_hw_channel + chip * n_channels_per_chip + if global_hw_chan in missing_hw_chans: + continue + ephys_channel_ids_list.append(local_hw_channel + chip * n_channels_per_chip) + return ephys_channel_ids_list def _parse_header(self): # parse file until "" @@ -126,7 +130,8 @@ def _parse_header(self): sconf = root.find("SpikeConfiguration") self._sampling_rate = float(hconf.attrib["samplingRate"]) - num_ephy_channels = int(hconf.attrib["numChannels"]) + num_ephy_channels_xml = int(hconf.attrib["numChannels"]) + num_ephy_channels = num_ephy_channels_xml # check for agreement with number of channels in xml sconf_channels = np.sum([len(x) for x in sconf]) @@ -220,7 +225,9 @@ def _parse_header(self): # we can only produce these channels for a subset of spikegadgets setup. If this criteria isn't # true then we should just use the raw_channel_ids and let the end user sort everything out if num_ephy_channels % num_chan_per_chip == 0: - channel_ids = self._produce_ephys_channel_ids(num_ephy_channels, num_chan_per_chip) + all_hw_chans = [int(schan.attrib["hwChan"]) for trode in sconf for schan in trode] + missing_hw_chans = set(range(num_ephy_channels)) - set(all_hw_chans) + channel_ids = self._produce_ephys_channel_ids(num_ephy_channels_xml, num_chan_per_chip, missing_hw_chans) raw_channel_ids = False else: raw_channel_ids = True diff --git a/neo/test/rawiotest/test_spikegadgetsrawio.py b/neo/test/rawiotest/test_spikegadgetsrawio.py index ecf3c5644..c960e221e 100644 --- a/neo/test/rawiotest/test_spikegadgetsrawio.py +++ b/neo/test/rawiotest/test_spikegadgetsrawio.py @@ -2,6 +2,7 @@ from neo.rawio.spikegadgetsrawio import SpikeGadgetsRawIO from neo.test.rawiotest.common_rawio_test import BaseTestRawIO +from numpy.testing import assert_array_equal class TestSpikeGadgetsRawIO( @@ -16,6 +17,38 @@ class TestSpikeGadgetsRawIO( "spikegadgets/SpikeGadgets_test_data_2xNpix1.0_20240318_173658.rec", ] +class TestSpikeGadgetsRawIOHeaderOnly(unittest.TestCase): + def setUp(self): + filename = "/Volumes/T7/CatalystNeuro/Jadhav/stubbed_files/SL18_D19_S01_F01_BOX_SLP_20230503_112642_stubbed.rec" + self.rawio = SpikeGadgetsRawIO(filename=filename) + + def test_parse_header(self): + self.rawio.parse_header() + assert_array_equal( + self.rawio.header['signal_channels']['id'], + [ + 'ECU_Ain1', 'ECU_Ain2', 'ECU_Ain3', 'ECU_Ain4', 'ECU_Ain5', 'ECU_Ain6', + 'ECU_Ain7', 'ECU_Ain8', 'ECU_Aout1', 'ECU_Aout2', 'ECU_Aout3', 'ECU_Aout4', '0', + '32', '96', '160', '192', '224', '1', '33', '65', '97', '161', '193', '225', '2', '34', + '98', '162', '194', '226', '3', '35', '67', '99', '163', '195', '227', '4', '36', + '100', '164', '196', '228', '5', '37', '69', '101', '165', '197', '229', '6', '38', + '102', '166', '198', '230', '7', '39', '71', '103', '167', '199', '231', '8', '40', + '72', '104', '136', '168', '200', '232', '9', '41', '73', '105', '137', '169', '201', + '233', '10', '42', '74', '106', '138', '170', '202', '234', '11', '43', '75', '107', + '139', '171', '203', '235', '12', '44', '76', '108', '140', '172', '204', '236', '13', + '45', '77', '109', '141', '173', '205', '237', '14', '46', '78', '110', '142', '174', + '206', '238', '15', '47', '79', '111', '143', '175', '207', '239', '80', '144', '176', + '208', '240', '17', '49', '81', '145', '177', '209', '241', '82', '146', '178', '210', + '242', '19', '51', '83', '147', '179', '211', '243', '84', '148', '180', '212', '244', + '21', '53', '85', '149', '181', '213', '245', '86', '150', '182', '214', '246', '23', + '55', '87', '151', '183', '215', '247', '24', '56', '88', '152', '184', '216', '248', + '25', '57', '89', '121', '153', '185', '217', '249', '26', '58', '90', '154', '186', + '218', '250', '27', '59', '91', '123', '155', '187', '219', '251', '28', '60', '92', + '156', '188', '220', '252', '29', '61', '93', '125', '157', '189', '221', '253', '30', + '62', '94', '158', '190', '222', '254', '31', '63', '95', '127', '159', '191', '223', + '255', + ] + ) if __name__ == "__main__": unittest.main()