From de78b97231b955bea7daf6707d9abf0fce4ee091 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Wed, 24 Jun 2026 14:12:21 -0600 Subject: [PATCH] channel deduplication --- neo/rawio/baserawio.py | 26 ++++++++++++++++++++++---- neo/test/rawiotest/test_axonrawio.py | 11 +++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/neo/rawio/baserawio.py b/neo/rawio/baserawio.py index 4207f3c03..2bb25f5c2 100644 --- a/neo/rawio/baserawio.py +++ b/neo/rawio/baserawio.py @@ -709,10 +709,28 @@ def _check_stream_signal_channel_characteristics(self): f"do not have the same {_common_sig_characteristics} {unique_characteristics}" ) - # also check that channel_id is unique inside a stream - channel_ids = signal_channels[mask]["id"] - if np.unique(channel_ids).size != channel_ids.size: - raise ValueError(f"signal_channels do not have unique ids for stream {stream_index}") + # Channel ids must be unique inside a stream so a channel can be addressed by id + # (see channel_id_to_index). Some files, e.g. re-saved exports, store duplicate ids. + # Rather than refuse an otherwise-readable file, make the duplicates unique by + # suffixing them and warn, so the data stays readable and id-based selection works. + channel_indexes = np.flatnonzero(mask) + channel_ids = list(signal_channels["id"][channel_indexes]) + if len(set(channel_ids)) != len(channel_ids): + used_ids = set() + deduplicated_ids = [] + for channel_id in channel_ids: + new_id = channel_id + suffix = 0 + while new_id in used_ids: + suffix += 1 + new_id = f"{channel_id}-{suffix}" + used_ids.add(new_id) + deduplicated_ids.append(new_id) + signal_channels["id"][channel_indexes] = deduplicated_ids + self.logger.warning( + f"signal_channels in stream {stream_index} have non-unique ids; duplicates were " + "made unique by suffixing so channels remain addressable by id." + ) self._several_channel_groups = signal_streams.size > 1 diff --git a/neo/test/rawiotest/test_axonrawio.py b/neo/test/rawiotest/test_axonrawio.py index a1cb7dd2b..d602d76ce 100644 --- a/neo/test/rawiotest/test_axonrawio.py +++ b/neo/test/rawiotest/test_axonrawio.py @@ -28,6 +28,17 @@ def test_read_raw_protocol(self): reader.read_raw_protocol() + def test_non_unique_channel_ids_are_deduplicated(self): + # Some ABF files (e.g. re-saved exports) store duplicate signal_channel ids. Rather than + # refuse an otherwise-readable file, neo makes the ids unique and warns, so the data stays + # readable and channels remain addressable by id. + path = self.get_local_path("axon/intracellular_data/files_with_errors/non_unique_channel_ids.abf") + reader = AxonRawIO(filename=path) + with self.assertLogs(reader.logger, level="WARNING"): + reader.parse_header() + channel_ids = list(reader.header["signal_channels"]["id"]) + self.assertEqual(len(channel_ids), len(set(channel_ids))) + if __name__ == "__main__": unittest.main()