Repository: SpikeInterface/spikeinterface
Pull request #3740 (2025-03-04 20:52:58)

Fix `aggregate_channels`  merging of channel ids with different string sub type 
This will fix @chrishalcrow bug on #3733.

The current check for `all_channels_have_same_type` is too strict. I added a test.


Patch:
diff --git a/src/spikeinterface/core/channelsaggregationrecording.py b/src/spikeinterface/core/channelsaggregationrecording.py
index 820b4fcd9..4fa1d8897 100644
--- a/src/spikeinterface/core/channelsaggregationrecording.py
+++ b/src/spikeinterface/core/channelsaggregationrecording.py
@@ -30,14 +30,17 @@ class ChannelsAggregationRecording(BaseRecording):
             ), "'renamed_channel_ids' doesn't have the right size or has duplicates!"
             channel_ids = list(renamed_channel_ids)
         else:
-            # Collect channel IDs from all recordings
-            all_channels_have_same_type = np.unique([rec.channel_ids.dtype for rec in recording_list]).size == 1
-            all_channel_ids_are_unique = False
-            if all_channels_have_same_type:
+
+            # Explicitly check if all channel_ids arrays are either all integers or all strings.
+            all_ids_are_int_dtype = all(np.issubdtype(rec.channel_ids.dtype, np.integer) for rec in recording_list)
+            all_ids_are_str_dtype = all(np.issubdtype(rec.channel_ids.dtype, np.str_) for rec in recording_list)
+
+            all_ids_have_same_dtype = all_ids_are_int_dtype or all_ids_are_str_dtype
+            if all_ids_have_same_dtype:
                 combined_ids = np.concatenate([rec.channel_ids for rec in recording_list])
                 all_channel_ids_are_unique = np.unique(combined_ids).size == num_all_channels
 
-            if all_channels_have_same_type and all_channel_ids_are_unique:
+            if all_ids_have_same_dtype and all_channel_ids_are_unique:
                 channel_ids = combined_ids
             else:
                 # If IDs are not unique or not of the same type, use default as stringify IDs
diff --git a/src/spikeinterface/core/channelslice.py b/src/spikeinterface/core/channelslice.py
index db15bd219..8a4f29e86 100644
--- a/src/spikeinterface/core/channelslice.py
+++ b/src/spikeinterface/core/channelslice.py
@@ -31,9 +31,12 @@ class ChannelSliceRecording(BaseRecording):
         parents_chan_ids = parent_recording.get_channel_ids()
 
         # some checks
-        assert all(
-            chan_id in parents_chan_ids for chan_id in self._channel_ids
-        ), "ChannelSliceRecording : channel ids are not all in parents"
+        # We use lists to compare numpy scalar types as their python versions (e.g. int vs int64())
+        channel_ids_not_in_parents = [id for id in self._channel_ids.tolist() if id not in parents_chan_ids.tolist()]
+        assert (
+            len(channel_ids_not_in_parents) == 0
+        ), f"ChannelSliceRecording : channel ids {channel_ids_not_in_parents} are not all in parent ids {parents_chan_ids}"
+
         assert len(self._channel_ids) == len(
             self._renamed_channel_ids
         ), "ChannelSliceRecording: renamed channel_ids must be the same size"


Test patch:
diff --git a/src/spikeinterface/core/tests/test_channelsaggregationrecording.py b/src/spikeinterface/core/tests/test_channelsaggregationrecording.py
index 99d6890df..16a91a55e 100644
--- a/src/spikeinterface/core/tests/test_channelsaggregationrecording.py
+++ b/src/spikeinterface/core/tests/test_channelsaggregationrecording.py
@@ -118,5 +118,25 @@ def test_channel_aggregation_does_not_preserve_ids_not_the_same_type():
     assert list(aggregated_recording.get_channel_ids()) == ["0", "1", "2", "3", "4"]
 
 
+def test_channel_aggregation_with_string_dtypes_of_different_size():
+    """
+    Fixes issue https://github.com/SpikeInterface/spikeinterface/issues/3733
+
+    This tests that the channel ids are propagated in the aggregation even if they are strings of different
+    string dtype sizes.
+    """
+    recording1 = generate_recording(num_channels=2, durations=[10], set_probe=False)
+    recording1 = recording1.rename_channels(new_channel_ids=np.array(["8", "9"], dtype="<U1"))
+
+    recording2 = generate_recording(num_channels=2, durations=[10], set_probe=False)
+    recording2 = recording2.rename_channels(new_channel_ids=np.array(["10", "11"], dtype="<U2"))
+
+    aggregated_recording = aggregate_channels([recording1, recording2])
+    assert aggregated_recording.get_num_channels() == 4
+    aggregated_recording_channel_ids = list(aggregated_recording.get_channel_ids())
+    assert aggregated_recording_channel_ids == ["8", "9", "10", "11"]
+    assert aggregated_recording.channel_ids.dtype == np.dtype("<U2")
+
+
 if __name__ == "__main__":
     test_channelsaggregationrecording()