Skip to content

Commit d297ee6

Browse files
authored
Merge pull request #3607 from chrishalcrow/fast-correlogram-merge
Add a fast correlogram merge
2 parents 9ed4974 + c0d22ad commit d297ee6

2 files changed

Lines changed: 154 additions & 3 deletions

File tree

src/spikeinterface/postprocessing/correlograms.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import warnings
44
import numpy as np
55
from spikeinterface.core.sortinganalyzer import register_result_extension, AnalyzerExtension, SortingAnalyzer
6+
from copy import deepcopy
67

78
from spikeinterface.core.waveforms_extractor_backwards_compatibility import MockWaveformExtractor
89

@@ -93,9 +94,98 @@ def _select_extension_data(self, unit_ids):
9394
def _merge_extension_data(
9495
self, merge_unit_groups, new_unit_ids, new_sorting_analyzer, censor_ms=None, verbose=False, **job_kwargs
9596
):
96-
# recomputing correlogram is fast enough and much easier in this case
97-
new_ccgs, new_bins = _compute_correlograms_on_sorting(new_sorting_analyzer.sorting, **self.params)
98-
new_data = dict(ccgs=new_ccgs, bins=new_bins)
97+
"""
98+
When two units are merged, their cross-correlograms with other units become the sum
99+
of the previous cross-correlograms. More precisely, if units i and j get merged into
100+
unit k, then the new unit's cross-correlogram with any other unit l is:
101+
C_{k,l} = C_{i,l} + C_{j,l}
102+
C_{l,k} = C_{l,k} + C_{l,j}
103+
Here, we apply this formula to quickly compute correlograms for merged units.
104+
"""
105+
106+
can_apply_soft_method = True
107+
if censor_ms is not None:
108+
# if censor_ms has no effect, can apply "soft" method. Check if any spikes have been removed
109+
for new_unit_id, merge_unit_group in zip(new_unit_ids, merge_unit_groups):
110+
111+
num_segments = new_sorting_analyzer.get_num_segments()
112+
for segment_index in range(num_segments):
113+
114+
merged_spike_train_length = len(
115+
new_sorting_analyzer.sorting.get_unit_spike_train(new_unit_id, segment_index=segment_index)
116+
)
117+
118+
old_spike_train_lengths = len(
119+
np.concatenate(
120+
[
121+
self.sorting_analyzer.sorting.get_unit_spike_train(unit_id, segment_index=segment_index)
122+
for unit_id in merge_unit_group
123+
]
124+
)
125+
)
126+
127+
if merged_spike_train_length != old_spike_train_lengths:
128+
can_apply_soft_method = False
129+
break
130+
131+
if can_apply_soft_method is False:
132+
new_ccgs, new_bins = _compute_correlograms_on_sorting(new_sorting_analyzer.sorting, **self.params)
133+
new_data = dict(ccgs=new_ccgs, bins=new_bins)
134+
else:
135+
136+
# Make a transformation dict, which tells us how unit_indices from the
137+
# old to the new sorter are mapped.
138+
old_to_new_unit_index_map = {}
139+
for old_unit in self.sorting_analyzer.unit_ids:
140+
old_unit_index = self.sorting_analyzer.sorting.id_to_index(old_unit)
141+
unit_involved_in_merge = False
142+
for merge_unit_group, new_unit_id in zip(merge_unit_groups, new_unit_ids):
143+
new_unit_index = new_sorting_analyzer.sorting.id_to_index(new_unit_id)
144+
# check if the old_unit is involved in a merge
145+
if old_unit in merge_unit_group:
146+
# check if it is mapped to itself
147+
if old_unit == new_unit_id:
148+
old_to_new_unit_index_map[old_unit_index] = new_unit_index
149+
# or to a unit_id outwith the old ones
150+
elif new_unit_id not in self.sorting_analyzer.unit_ids:
151+
if new_unit_index not in old_to_new_unit_index_map.values():
152+
old_to_new_unit_index_map[old_unit_index] = new_unit_index
153+
unit_involved_in_merge = True
154+
if unit_involved_in_merge is False:
155+
old_to_new_unit_index_map[old_unit_index] = new_sorting_analyzer.sorting.id_to_index(old_unit)
156+
157+
need_to_append = False
158+
delete_from = 1
159+
160+
correlograms, new_bins = deepcopy(self.get_data())
161+
162+
for new_unit_id, merge_unit_group in zip(new_unit_ids, merge_unit_groups):
163+
164+
merge_unit_group_indices = self.sorting_analyzer.sorting.ids_to_indices(merge_unit_group)
165+
166+
# Sum unit rows of the correlogram matrix: C_{k,l} = C_{i,l} + C_{j,l}
167+
# and place this sum in all indices from the merge group
168+
new_col = np.sum(correlograms[merge_unit_group_indices, :, :], axis=0)
169+
# correlograms[merge_unit_group_indices[0], :, :] = new_col
170+
correlograms[merge_unit_group_indices, :, :] = new_col
171+
# correlograms[merge_unit_group_indices[1:], :, :] = 0
172+
173+
# Sum unit columns of the correlogram matrix: C_{l,k} = C_{l,i} + C_{l,j}
174+
# and put this sum in all indices from the merge group
175+
new_row = np.sum(correlograms[:, merge_unit_group_indices, :], axis=1)
176+
177+
for merge_unit_group_index in merge_unit_group_indices:
178+
correlograms[:, merge_unit_group_index, :] = new_row
179+
180+
new_correlograms = np.zeros(
181+
(len(new_sorting_analyzer.unit_ids), len(new_sorting_analyzer.unit_ids), correlograms.shape[2])
182+
)
183+
for old_index_1, new_index_1 in old_to_new_unit_index_map.items():
184+
for old_index_2, new_index_2 in old_to_new_unit_index_map.items():
185+
new_correlograms[new_index_1, new_index_2, :] = correlograms[old_index_1, old_index_2, :]
186+
new_correlograms[new_index_2, new_index_1, :] = correlograms[old_index_2, old_index_1, :]
187+
188+
new_data = dict(ccgs=new_correlograms, bins=new_bins)
99189
return new_data
100190

101191
def _run(self, verbose=False):
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import numpy as np
2+
3+
from spikeinterface.core import generate_ground_truth_recording, create_sorting_analyzer
4+
5+
6+
def test_correlograms_merge():
7+
"""
8+
When merging in `soft` mode, correlograms sum and we take advantage of this to make
9+
a fast computation. This test checks that we get the same result using this fast
10+
sum as recomputing the correlograms from scratch.
11+
"""
12+
13+
rec, sort = generate_ground_truth_recording(durations=[10, 10])
14+
15+
sorting_analyzer = create_sorting_analyzer(recording=rec, sorting=sort)
16+
sorting_analyzer.compute("correlograms")
17+
18+
trial_merges = [
19+
[["1", "2"]],
20+
[["2", "4", "6", "8"]],
21+
[["1", "4", "7"], ["2", "8"]],
22+
[["4", "1", "8"], ["2", "7", "0"], ["3", "9"], ["5", "6"]],
23+
]
24+
25+
new_unit_ids = [["2"], ["4"], ["4", "2"], ["1", "2", "9", "100"]]
26+
27+
for new_id_strategy in ["append", "take_first", "user"]:
28+
for merge_unit_groups, new_unit_id in zip(trial_merges, new_unit_ids):
29+
30+
# first, compute the correlograms of the merged units using the merge method
31+
if new_id_strategy == "user":
32+
merged_sorting_analyzer = sorting_analyzer.merge_units(
33+
merge_unit_groups=merge_unit_groups, new_unit_ids=new_unit_id
34+
)
35+
else:
36+
merged_sorting_analyzer = sorting_analyzer.merge_units(
37+
merge_unit_groups=merge_unit_groups, new_id_strategy=new_id_strategy
38+
)
39+
computed_correlograms = merged_sorting_analyzer.get_extension("correlograms").get_data()
40+
41+
# Then re-compute, and compare
42+
recomputed_correlograms = merged_sorting_analyzer.compute("correlograms").get_data()
43+
assert np.all(computed_correlograms[0] == recomputed_correlograms[0])
44+
45+
# test when `censor_ms` is not None. This merge does remove some spikes.
46+
merged_sorting_analyzer_censored = sorting_analyzer.merge_units(
47+
merge_unit_groups=trial_merges[2], new_id_strategy="take_first", censor_ms=5
48+
)
49+
computed_ccgs_censored = merged_sorting_analyzer_censored.get_extension("correlograms").get_data()
50+
51+
recomputed_ccgs_censored = merged_sorting_analyzer_censored.compute("correlograms").get_data()
52+
assert np.all(computed_ccgs_censored[0] == recomputed_ccgs_censored[0])
53+
54+
# This `censor_ms` does not remove spikes, so can use the soft method
55+
merged_sorting_analyzer_not_censored = sorting_analyzer.merge_units(
56+
merge_unit_groups=trial_merges[0], new_id_strategy="take_first", censor_ms=0
57+
)
58+
computed_ccgs_not_censored = merged_sorting_analyzer_not_censored.get_extension("correlograms").get_data()
59+
60+
recomputed_ccgs_not_censored = merged_sorting_analyzer_not_censored.compute("correlograms").get_data()
61+
assert np.all(computed_ccgs_not_censored[0] == recomputed_ccgs_not_censored[0])

0 commit comments

Comments
 (0)