Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
dd8f443
low level auto merge using template similarity for sorting components.
samuelgarcia May 14, 2025
3fcd9d7
improve auto merge in tdc_lustering and cicurs_clustering
samuelgarcia May 15, 2025
90e0b14
improve drift aware clustering tdc
samuelgarcia May 16, 2025
3653c81
Merge branch 'main' of github.com:SpikeInterface/spikeinterface into …
samuelgarcia May 16, 2025
221e780
add with_template=False in BenchmarkClustering.compute_result
samuelgarcia Jun 4, 2025
0edfc77
oups
samuelgarcia Jun 13, 2025
bed91f2
update tdc
samuelgarcia Jun 16, 2025
ea7abd8
oups
samuelgarcia Jun 16, 2025
3ae7e6e
oups
samuelgarcia Jun 16, 2025
5c65137
fix
samuelgarcia Jun 16, 2025
811c0f1
tests
samuelgarcia Jun 16, 2025
bfa6e23
Merge branch 'main' into components_merge_templates
samuelgarcia Jun 17, 2025
6a24e8b
merge main and fixes
samuelgarcia Jun 17, 2025
b1ec837
Merge branch 'main' of github.com:SpikeInterface/spikeinterface into …
samuelgarcia Jun 17, 2025
0102ad9
clean
samuelgarcia Jun 17, 2025
5c9b641
Fix MatchingStudy.plot_collisions
samuelgarcia Jun 17, 2025
d1ba03d
small fixes in circus-clustering
samuelgarcia Jun 17, 2025
aa1a6f3
speedup the collision comparison and benchmarkmatching
samuelgarcia Jun 18, 2025
861f73a
wip
samuelgarcia Jun 23, 2025
bef679b
Merge branch 'main' of github.com:SpikeInterface/spikeinterface into …
samuelgarcia Jun 25, 2025
6129df5
Better sparsity for analyzer in benchmarks
samuelgarcia Jun 25, 2025
1b88a97
Fix some etra_outputs in clustering methods.
samuelgarcia Jun 25, 2025
2462ae5
merge conflict
samuelgarcia Jun 25, 2025
2d3d654
more fix in comparison
samuelgarcia Jun 25, 2025
9d93aaf
improve plot_performances_vs_snr()
samuelgarcia Jul 8, 2025
8150c47
improve tdridesclous2
samuelgarcia Jul 8, 2025
783cd8f
wip circus clustering
samuelgarcia Jul 8, 2025
97762b5
Merge branch 'main' of github.com:SpikeInterface/spikeinterface into …
samuelgarcia Jul 8, 2025
867dc3e
Improve tridesclous2
samuelgarcia Jul 10, 2025
b823a97
Merge branch 'main' of github.com:SpikeInterface/spikeinterface into …
samuelgarcia Jul 10, 2025
0729ddf
Fixes for the merging branch of Sam, mostly for SC2 purposes and to t…
yger Jul 10, 2025
a9bb5f6
clean
samuelgarcia Jul 10, 2025
7ca35a0
Add isosplit for tests
samuelgarcia Jul 10, 2025
8027150
Merge branch 'main' of github.com:SpikeInterface/spikeinterface into …
samuelgarcia Jul 11, 2025
620076a
improve circus
yger Jul 11, 2025
f3eef33
Merge branch 'components_merge_templates' of github.com:samuelgarcia/…
samuelgarcia Jul 11, 2025
e50bda1
handling when isoplit6 is not installable in tridesclous2
samuelgarcia Jul 11, 2025
7794728
warn instead of raise for motion and spatial windows
samuelgarcia Jul 11, 2025
9dcdfcd
levels_to_keep > levels_to_group_by in benchmarks
samuelgarcia Jul 11, 2025
a4e8f78
benchmark improvs plot_unit_counts and plot_run_times
samuelgarcia Jul 16, 2025
7c93185
benchmark : replace seaborn by matplotlib for better cosmetic control
samuelgarcia Jul 17, 2025
6cf2248
plot_count_unit improvement
samuelgarcia Jul 17, 2025
ac091d2
Small fixes
samuelgarcia Jul 18, 2025
6eab417
optional installation of kilosort4like sorter (experimental and hidde…
samuelgarcia Jul 18, 2025
c944850
debug order of import and Template partially imported
samuelgarcia Jul 21, 2025
5b27515
Merge branch 'main' of github.com:SpikeInterface/spikeinterface into …
samuelgarcia Jul 24, 2025
d1a7735
merge with main
samuelgarcia Jul 24, 2025
255d25c
fix tests
samuelgarcia Jul 24, 2025
af41d07
clean_template()
yger Jul 25, 2025
e37754b
clean_template in tridesclous2
samuelgarcia Jul 25, 2025
9a12364
fix motion tests
samuelgarcia Jul 25, 2025
edb2802
fix test when no isosplit6
samuelgarcia Jul 25, 2025
07bd8bc
merge and fix
samuelgarcia Jul 28, 2025
2693641
fix launcher
samuelgarcia Jul 28, 2025
2845809
skip tdc-clustering
samuelgarcia Jul 28, 2025
ebbd182
oups
samuelgarcia Jul 28, 2025
37e3734
fix conflict
samuelgarcia Jul 28, 2025
c8c02de
Merge branch 'main' into components_merge_templates
samuelgarcia Jul 28, 2025
6f09e17
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 28, 2025
0a39b12
tdc2 and sc2 versions are now yyyy.mm
samuelgarcia Jul 28, 2025
163bb68
Merge branch 'components_merge_templates' of github.com:samuelgarcia/…
samuelgarcia Jul 28, 2025
c8856eb
oups
samuelgarcia Jul 28, 2025
9e23a9e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 28, 2025
78a0ac1
try debug sc2 windows
samuelgarcia Jul 28, 2025
da9dd6a
Merge branch 'components_merge_templates' of github.com:samuelgarcia/…
samuelgarcia Jul 28, 2025
898f6d6
try debug sc2 windows
samuelgarcia Jul 28, 2025
1af1ebb
try debug sc2 windows
samuelgarcia Jul 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/spikeinterface/postprocessing/template_similarity.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,12 +333,17 @@ def compute_similarity_with_templates_array(
mask = np.ones((num_templates, other_num_templates, num_channels), dtype=bool)

if sparsity is not None and other_sparsity is not None:

# make the input more flexible with either The object or the array mask
sparsity_mask = sparsity.mask if isinstance(sparsity, ChannelSparsity) else sparsity
other_sparsity_mask = other_sparsity.mask if isinstance(other_sparsity, ChannelSparsity) else other_sparsity

if support == "intersection":
mask = np.logical_and(sparsity.mask[:, np.newaxis, :], other_sparsity.mask[np.newaxis, :, :])
mask = np.logical_and(sparsity_mask[:, np.newaxis, :], other_sparsity_mask[np.newaxis, :, :])
elif support == "union":
mask = np.logical_and(sparsity.mask[:, np.newaxis, :], other_sparsity.mask[np.newaxis, :, :])
mask = np.logical_and(sparsity_mask[:, np.newaxis, :], other_sparsity_mask[np.newaxis, :, :])
units_overlaps = np.sum(mask, axis=2) > 0
mask = np.logical_or(sparsity.mask[:, np.newaxis, :], other_sparsity.mask[np.newaxis, :, :])
mask = np.logical_or(sparsity_mask[:, np.newaxis, :], other_sparsity_mask[np.newaxis, :, :])
mask[~units_overlaps] = False

assert num_shifts < num_samples, "max_lag is too large"
Expand Down
2 changes: 1 addition & 1 deletion src/spikeinterface/sorters/internal/spyking_circus2.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ def _run_from_folder(cls, sorter_output_folder, params, verbose):
_, peak_labels, svd_model, svd_features, sparsity_mask = outputs
from spikeinterface.sortingcomponents.clustering.tools import get_templates_from_peaks_and_svd

templates = get_templates_from_peaks_and_svd(
templates, _ = get_templates_from_peaks_and_svd(
recording_w,
selected_peaks,
peak_labels,
Expand Down
88 changes: 52 additions & 36 deletions src/spikeinterface/sortingcomponents/clustering/circus.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@
HAVE_HDBSCAN = False

import random, string
from spikeinterface.core import get_global_tmp_folder
from spikeinterface.core.basesorting import minimum_spike_dtype
from spikeinterface.core.waveform_tools import estimate_templates
from spikeinterface.core import get_global_tmp_folder, Templates
from .clustering_tools import remove_duplicates_via_matching
from spikeinterface.core.recording_tools import get_noise_levels, get_channel_distances
from spikeinterface.sortingcomponents.peak_selection import select_peaks
from spikeinterface.core.template import Templates
from spikeinterface.core.sparsity import compute_sparsity
from spikeinterface.sortingcomponents.tools import remove_empty_templates
from spikeinterface.sortingcomponents.clustering.peak_svd import extract_peaks_svd

from spikeinterface.sortingcomponents.clustering.merge import merge_peak_labels_from_templates

from spikeinterface.sortingcomponents.tools import extract_waveform_at_max_channel

Expand Down Expand Up @@ -56,11 +54,18 @@ class CircusClustering:
"few_waveforms": None,
"ms_before": 0.5,
"ms_after": 0.5,
"remove_small_snr": False,
Comment thread
samuelgarcia marked this conversation as resolved.
Outdated
"noise_threshold": 4,
"rank": 5,
"templates_from_svd": False,
"noise_levels": None,
"tmp_folder": None,
"do_merge": True,
"merge_kwargs": {
"similarity_metric": "l1",
"num_shifts": 3,
"similarity_thresh": 0.8,
},
"verbose": True,
"debug": False,
}
Expand Down Expand Up @@ -170,10 +175,11 @@ def main_function(cls, recording, peaks, params, job_kwargs=dict()):
ms_after,
**job_kwargs,
)
sparse_mask2 = sparse_mask
else:
from spikeinterface.sortingcomponents.clustering.tools import get_templates_from_peaks_and_svd

templates = get_templates_from_peaks_and_svd(
templates, sparse_mask2 = get_templates_from_peaks_and_svd(
recording,
peaks,
peak_labels,
Expand All @@ -185,44 +191,54 @@ def main_function(cls, recording, peaks, params, job_kwargs=dict()):
operator="median",
)

templates_array = templates.templates_array
best_channels = np.argmax(np.abs(templates_array[:, nbefore, :]), axis=1)
peak_snrs = np.abs(templates_array[:, nbefore, :])
best_snrs_ratio = (peak_snrs / params["noise_levels"])[np.arange(len(peak_snrs)), best_channels]
old_unit_ids = templates.unit_ids.copy()
valid_templates = best_snrs_ratio > params["noise_threshold"]

mask = np.isin(peak_labels, old_unit_ids[~valid_templates])
peak_labels[mask] = -1

from spikeinterface.core.template import Templates

templates = Templates(
templates_array=templates_array[valid_templates],
sampling_frequency=fs,
nbefore=templates.nbefore,
sparsity_mask=None,
channel_ids=recording.channel_ids,
unit_ids=templates.unit_ids[valid_templates],
probe=recording.get_probe(),
is_scaled=False,
)
if params["do_merge"]:
peak_labels, merge_template_array, new_unit_ids = merge_peak_labels_from_templates(
peaks, peak_labels, templates.unit_ids,
templates.templates_array, sparse_mask2,
**params["merge_kwargs"]
)

templates = Templates(
templates_array=merge_template_array,
sampling_frequency=fs,
nbefore=templates.nbefore,
sparsity_mask=None,
channel_ids=recording.channel_ids,
unit_ids=new_unit_ids,
probe=recording.get_probe(),
is_scaled=False
)

if params["remove_small_snr"] :
templates_array = templates.templates_array
best_channels = np.argmax(np.abs(templates_array[:, nbefore, :]), axis=1)
peak_snrs = np.abs(templates_array[:, nbefore, :])
best_snrs_ratio = (peak_snrs / params["noise_levels"])[np.arange(len(peak_snrs)), best_channels]
old_unit_ids = templates.unit_ids.copy()
valid_templates = best_snrs_ratio > params["noise_threshold"]

mask = np.isin(peak_labels, old_unit_ids[~valid_templates])
peak_labels[mask] = -1

templates = templates.select_units(templates.unit_ids[valid_templates])

labels = templates.unit_ids

if params["debug"]:
templates_folder = tmp_folder / "dense_templates"
templates.to_zarr(folder_path=templates_folder)

sparsity = compute_sparsity(templates, noise_levels=params["noise_levels"], **params["sparsity"])
templates = templates.to_sparse(sparsity)
empty_templates = templates.sparsity_mask.sum(axis=1) == 0
old_unit_ids = templates.unit_ids.copy()
templates = remove_empty_templates(templates)
# sparsity = compute_sparsity(templates, noise_levels=params["noise_levels"], **params["sparsity"])
# templates = templates.to_sparse(sparsity)
# empty_templates = templates.sparsity_mask.sum(axis=1) == 0
# old_unit_ids = templates.unit_ids.copy()
# templates = remove_empty_templates(templates)

mask = np.isin(peak_labels, old_unit_ids[empty_templates])
peak_labels[mask] = -1
# mask = np.isin(peak_labels, old_unit_ids[empty_templates])
# peak_labels[mask] = -1

labels = np.unique(peak_labels)
labels = labels[labels >= 0]
# labels = np.unique(peak_labels)
# labels = labels[labels >= 0]

if params["remove_mixtures"]:
if verbose:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def main_function(cls, recording, peaks, params, job_kwargs=dict()):
ms_after=ms_after,
radius_um=radius_um,
motion_aware=motion_aware,
motion=None,
motion=motion,
**params["extract_peaks_svd_kwargs"],
# **job_kwargs
)
Expand Down
69 changes: 69 additions & 0 deletions src/spikeinterface/sortingcomponents/clustering/merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,3 +754,72 @@ def merge(
NormalizedTemplateDiff,
]
find_pair_method_dict = {e.name: e for e in find_pair_method_list}



def merge_peak_labels_from_templates(peaks, peak_labels, unit_ids,
templates_array, sparsity_mask,
similarity_metric="l1",
similarity_thresh=0.8,
num_shifts=3,
):
assert len(unit_ids) == templates_array.shape[0]
Comment thread
samuelgarcia marked this conversation as resolved.

from spikeinterface.postprocessing.template_similarity import compute_similarity_with_templates_array
from scipy.sparse.csgraph import connected_components


similarity = compute_similarity_with_templates_array(
templates_array,
templates_array,
method=similarity_metric,
num_shifts=num_shifts,
support="union",
sparsity=sparsity_mask,
other_sparsity=sparsity_mask,
)
pair_mask = similarity > similarity_thresh

# import matplotlib.pyplot as plt
# fig, ax = plt.subplots()
# ax.hist(similarity.flatten(), bins=np.linspace(0, 1, 50), log=True)
# ax.axvline(similarity_thresh)


keep_template = np.ones(templates_array.shape[0], dtype="bool")
clean_labels = peak_labels.copy()
n_components, group_labels = connected_components(pair_mask, directed=False, return_labels=True)


# print("merges", templates_array.shape[0], "to", n_components)

merge_template_array = templates_array.copy()
new_unit_ids = np.zeros(n_components, dtype=unit_ids.dtype)
for c in range(n_components):
merge_group = np.flatnonzero(group_labels == c)
g0 = merge_group[0]
new_unit_ids[c] = unit_ids[g0]
if len(merge_group) > 1:
weights = np.zeros(len(merge_group), dtype=np.float32)

# import matplotlib.pyplot as plt
# fig, ax = plt.subplots()
# for i, l in enumerate(merge_group):
# temp_flat = merge_template_array[l, :, :].T.flatten()
# ax.plot(temp_flat)
# sim = similarity[merge_group[0], merge_group[1]]
# ax.set_title(f"{sim} {similarity_thresh}")


for i, l in enumerate(merge_group):
label = unit_ids[l]
weights[i] = np.sum(peak_labels == label)
if i > 0:
clean_labels[peak_labels == label] = unit_ids[g0]
keep_template[l] = False
weights /= weights.sum()
merge_template_array[g0, :, :] = np.sum(merge_template_array[merge_group, :, :] * weights[:, np.newaxis, np.newaxis], axis=0)


return clean_labels, merge_template_array, new_unit_ids

Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
# "sliding_nn": SlidingNNClustering,
"position_and_features": PositionAndFeaturesClustering,
"random_projections": RandomProjectionClustering,
"circus": CircusClustering,
"circus_clustering": CircusClustering,
"tdc_clustering": TdcClustering,
"graph_clustering": GraphClustering,
}
Expand Down
Loading