Skip to content

Commit ea747ac

Browse files
committed
fix: Correct and speed up multi-segment synchrony metrics
1 parent 80beb93 commit ea747ac

2 files changed

Lines changed: 56 additions & 22 deletions

File tree

src/spikeinterface/metrics/quality/misc_metrics.py

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,6 +1914,9 @@ def _get_synchrony_counts(spikes, synchrony_sizes, all_unit_ids):
19141914
----------
19151915
spikes : np.array
19161916
Structured numpy array with fields ("sample_index", "unit_index", "segment_index").
1917+
Must be ordered by segment_index and then by sample_index within each segment, as
1918+
returned by `BaseSorting.to_spike_vector()`; a spike sharing (segment_index,
1919+
sample_index) with a matching-event neighbor it is not adjacent to will not be counted as synchronous.
19171920
all_unit_ids : list or None, default: None
19181921
List of unit ids to compute the synchrony metrics. Expecting all units.
19191922
synchrony_sizes : None or np.array, default: None
@@ -1931,29 +1934,38 @@ def _get_synchrony_counts(spikes, synchrony_sizes, all_unit_ids):
19311934
"""
19321935

19331936
synchrony_counts = np.zeros((np.size(synchrony_sizes), len(all_unit_ids)), dtype=np.int64)
1937+
if spikes.size == 0:
1938+
return synchrony_counts
19341939

1935-
# compute the occurrence of each sample_index. Count >2 means there's synchrony
1936-
_, unique_spike_index, counts = np.unique(spikes["sample_index"], return_index=True, return_counts=True)
1937-
1938-
min_synchrony = 2
1939-
mask = counts >= min_synchrony
1940-
sync_indices = unique_spike_index[mask]
1941-
sync_counts = counts[mask]
1942-
1943-
all_syncs = np.unique(sync_counts)
1944-
num_bins = [np.size(synchrony_sizes[synchrony_sizes <= i]) for i in all_syncs]
1945-
1946-
indices = {}
1947-
for num_of_syncs in all_syncs:
1948-
indices[num_of_syncs] = np.flatnonzero(all_syncs == num_of_syncs)[0]
1949-
1950-
for i, sync_index in enumerate(sync_indices):
1951-
1952-
num_of_syncs = sync_counts[i]
1953-
# Counts inclusively. E.g. if there are 3 simultaneous spikes, these are also added
1954-
# to the 2 simultaneous spike bins.
1955-
units_with_sync = spikes[sync_index : sync_index + num_of_syncs]["unit_index"]
1956-
synchrony_counts[: num_bins[indices[num_of_syncs]], units_with_sync] += 1
1940+
sample_indices = spikes["sample_index"]
1941+
segment_indices = spikes["segment_index"]
1942+
same_segment_and_sample = (sample_indices[1:] == sample_indices[:-1]) & (
1943+
segment_indices[1:] == segment_indices[:-1]
1944+
)
1945+
synchronous_spike_mask = np.zeros(spikes.size, dtype=bool)
1946+
synchronous_spike_mask[:-1] |= same_segment_and_sample
1947+
synchronous_spike_mask[1:] |= same_segment_and_sample
1948+
if not np.any(synchronous_spike_mask):
1949+
return synchrony_counts
1950+
1951+
synchronous_sample_indices = sample_indices[synchronous_spike_mask]
1952+
synchronous_segment_indices = segment_indices[synchronous_spike_mask]
1953+
synchronous_units = spikes["unit_index"][synchronous_spike_mask]
1954+
synchronous_group_starts = np.empty(synchronous_units.size, dtype=bool)
1955+
synchronous_group_starts[0] = True
1956+
synchronous_group_starts[1:] = (synchronous_sample_indices[1:] != synchronous_sample_indices[:-1]) | (
1957+
synchronous_segment_indices[1:] != synchronous_segment_indices[:-1]
1958+
)
1959+
synchronous_group_indices = np.cumsum(synchronous_group_starts, dtype=np.int64) - 1
1960+
synchronous_group_counts = np.bincount(synchronous_group_indices)
1961+
1962+
group_unit_keys = synchronous_group_indices * len(all_unit_ids) + synchronous_units
1963+
unique_group_unit_keys = np.unique(group_unit_keys)
1964+
unique_group_indices, unique_unit_indices = np.divmod(unique_group_unit_keys, len(all_unit_ids))
1965+
num_bins = np.searchsorted(synchrony_sizes, synchronous_group_counts[unique_group_indices], side="right")
1966+
for synchrony_index in range(synchrony_sizes.size):
1967+
units = unique_unit_indices[num_bins > synchrony_index]
1968+
synchrony_counts[synchrony_index] = np.bincount(units, minlength=len(all_unit_ids))
19571969

19581970
return synchrony_counts
19591971

src/spikeinterface/metrics/quality/tests/test_metrics_functions.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
NumpySorting,
99
synthetize_spike_train_bad_isi,
1010
add_synchrony_to_sorting,
11+
generate_recording,
1112
generate_ground_truth_recording,
1213
create_sorting_analyzer,
1314
synthesize_random_firings,
@@ -213,6 +214,27 @@ def test_synchrony_counts_not_all_units():
213214
assert np.all(sync_count[0] == np.array([0, 1, 1]))
214215

215216

217+
def test_synchrony_metrics_do_not_cross_segments():
218+
sampling_frequency = 1_000.0
219+
samples_list = [[100, 200], [100, 100]]
220+
labels_list = [[0, 1], [2, 3]]
221+
sorting = NumpySorting.from_samples_and_labels(samples_list, labels_list, sampling_frequency, unit_ids=[0, 1, 2, 3])
222+
recording = generate_recording(
223+
durations=[1.0, 1.0],
224+
sampling_frequency=sampling_frequency,
225+
num_channels=4,
226+
seed=1205,
227+
)
228+
sorting_analyzer = create_sorting_analyzer(sorting, recording, format="memory", sparse=False)
229+
230+
synchrony_metrics = compute_synchrony_metrics(sorting_analyzer)
231+
232+
expected_sync_spike_2 = {0: 0.0, 1: 0.0, 2: 1.0, 3: 1.0}
233+
assert synchrony_metrics.sync_spike_2 == pytest.approx(expected_sync_spike_2)
234+
assert np.all(np.array(list(synchrony_metrics.sync_spike_4.values())) == 0)
235+
assert np.all(np.array(list(synchrony_metrics.sync_spike_8.values())) == 0)
236+
237+
216238
def test_mahalanobis_metrics():
217239
all_pcs1, all_labels1 = create_ground_truth_pc_distributions([1, -1], [1000, 1000])
218240
all_pcs2, all_labels2 = create_ground_truth_pc_distributions(

0 commit comments

Comments
 (0)