From 8a056b62ff3eb57949cd2b794cd42fd56c834545 Mon Sep 17 00:00:00 2001 From: Graham Findlay Date: Thu, 12 Nov 2026 16:47:35 -0600 Subject: [PATCH] Count overlapping periods once in compute_total_samples_per_unit --- .../spiketrain/tests/test_metric_functions.py | 5 +++++ src/spikeinterface/metrics/utils.py | 14 +++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/spikeinterface/metrics/spiketrain/tests/test_metric_functions.py b/src/spikeinterface/metrics/spiketrain/tests/test_metric_functions.py index 7577c767d6..061c2274b1 100644 --- a/src/spikeinterface/metrics/spiketrain/tests/test_metric_functions.py +++ b/src/spikeinterface/metrics/spiketrain/tests/test_metric_functions.py @@ -27,6 +27,11 @@ def test_calculate_firing_rates(sorting_analyzer_simple): firing_rates_periods = compute_firing_rates(sorting_analyzer, periods=periods) assert firing_rates == firing_rates_periods + # Overlapping periods shouldn't double-count time: + # the same periods twice should give the same firing rates as the original periods + firing_rates_overlapping = compute_firing_rates(sorting_analyzer, periods=np.concatenate([periods, periods])) + assert firing_rates_overlapping == firing_rates + # calculate num spikes with empty periods empty_periods = np.empty(0, dtype=unit_period_dtype) firing_rates_empty_periods = compute_firing_rates(sorting_analyzer, periods=empty_periods) diff --git a/src/spikeinterface/metrics/utils.py b/src/spikeinterface/metrics/utils.py index 9945861bda..0377a268eb 100644 --- a/src/spikeinterface/metrics/utils.py +++ b/src/spikeinterface/metrics/utils.py @@ -83,7 +83,7 @@ def compute_total_samples_per_unit(sorting_analyzer, periods=None): sorting_analyzer : SortingAnalyzer The sorting analyzer object. periods : array of unit_period_dtype, default: None - Periods to consider for each unit. + Periods to consider for each unit. Where a unit's periods overlap, the overlapping samples count once. Returns ------- @@ -93,9 +93,17 @@ def compute_total_samples_per_unit(sorting_analyzer, periods=None): if periods is not None: total_samples_array = np.zeros(len(sorting_analyzer.unit_ids), dtype="int64") sorting = sorting_analyzer.sorting - for period in periods: + # count the union of each unit's periods + order = np.lexsort((periods["start_sample_index"], periods["segment_index"], periods["unit_index"])) + covered_until = {} # (unit_index, segment_index) -> end of that unit's periods so far + for period in periods[order]: unit_index = period["unit_index"] - total_samples_array[unit_index] += period["end_sample_index"] - period["start_sample_index"] + key = (unit_index, period["segment_index"]) + start = max(period["start_sample_index"], covered_until.get(key, period["start_sample_index"])) + end = period["end_sample_index"] + if end > start: + total_samples_array[unit_index] += end - start + covered_until[key] = end total_samples = dict(zip(sorting.unit_ids, total_samples_array)) else: total = sorting_analyzer.get_total_samples()