diff --git a/README.md b/README.md index 655f3e02c3..444df1ce97 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Please [Star](https://github.com/SpikeInterface/spikeinterface/stargazers) the p SpikeInterface is a Python package designed to unify preexisting spike sorting technologies into a single code base. If you use SpikeInterface, you are also using code and ideas from many other projects. Our codebase would be tiny without the amazing algorithms and formats that we interface with. See them all, and how to cite them, on our [references page](https://spikeinterface.readthedocs.io/en/latest/references.html). In the past year, we have added support for the following tools: +- dartsort [DARTsort: A modular drift tracking spike sorter for high-density multi-electrode probes](https://www.biorxiv.org/content/10.1101/2023.08.11.553023v1) ([docs](https://spikeinterface.readthedocs.io/en/stable/modules/sorters.html#supported-spike-sorters)) - Bombcell [Bombcell: automated curation and cell classification of spike-sorted electrophysiology data](https://doi.org/10.5281/zenodo.8172822>) ([docs](https://spikeinterface.readthedocs.io/en/latest/how_to/auto_label_units.html#bombcell)) - SLAy. [SLAy-ing oversplitting errors in high-density electrophysiology spike sorting](https://www.biorxiv.org/content/10.1101/2025.06.20.660590v2) ([docs](https://spikeinterface.readthedocs.io/en/latest/modules/curation.html#auto-merging-units)) - Lupin, Spykingcircus2 and Tridesclous2. [Opening the black box: a modular approach to spike sorting](https://www.biorxiv.org/content/10.64898/2026.01.23.701239v1) ([docs](https://spikeinterface.readthedocs.io/en/stable/modules/sorters.html#supported-spike-sorters)) diff --git a/doc/index.rst b/doc/index.rst index f0f72d9981..46dbd674c5 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -12,6 +12,7 @@ amazing algorithms and formats that we interface with. See them all, and how to `references page `_. In the past year, we have added support for the following tools: +- *dartsort*. `DARTsort: A modular drift tracking spike sorter for high-density multi-electrode probes `_ (`docs `_) - Bombcell. `Bombcell: automated curation and cell classification of spike-sorted electrophysiology data. `_ (`docs `_) - SLAy. `SLAy-ing oversplitting errors in high-density electrophysiology spike sorting `_ (`docs `_) - Lupin, Spykingcicus2 and Tridesclous2. `Opening the black box: a modular approach to spike sorting `_ (`docs `_) diff --git a/doc/modules/sorters.rst b/doc/modules/sorters.rst index d507363b47..5c67296794 100644 --- a/doc/modules/sorters.rst +++ b/doc/modules/sorters.rst @@ -470,6 +470,7 @@ versions. Here is the list of external sorters accessible using the run_sorter wrapper: +* **dartsort** :code:`run_sorter(sorter_name='dartsort')` * **HerdingSpikes2** :code:`run_sorter(sorter_name='herdingspikes')` * **IronClust** :code:`run_sorter(sorter_name='ironclust')` * **Kilosort** :code:`run_sorter(sorter_name='kilosort')` diff --git a/doc/references.rst b/doc/references.rst index 6a17cbb6dc..1097bba6dd 100644 --- a/doc/references.rst +++ b/doc/references.rst @@ -38,6 +38,7 @@ If you use one of the following spike sorting algorithms (i.e. you use the :code please include the appropriate citation for the :code:`sorter_name` parameter you use: *Note: unless otherwise stated, the reference given is to be used for all versions of the sorter* +- :code:`dartsort` [Boussard2023]_ - :code:`combinato` [Niediek]_ - :code:`hdsort` [Diggelmann]_ - :code:`herdingspikes` [Muthmann]_ [Hilgen]_ @@ -110,6 +111,8 @@ References .. [Boussard] `Three-dimensional spike localization and imporved motion correction for Neuropixels recordings. 2021 `_ +.. [Boussard2023] `DARTsort: A modular drift tracking spike sorter for high-density multi-electrode probes. 2023 `_ + .. [Buccino] `SpikeInterface, a unified framework for spike sorting. 2020. `_ .. [Buzsáki] `The Log-Dynamic Brain: How Skewed Distributions Affect Network Operations. 2014. `_ diff --git a/src/spikeinterface/sorters/external/dartsort.py b/src/spikeinterface/sorters/external/dartsort.py new file mode 100644 index 0000000000..daad3ec555 --- /dev/null +++ b/src/spikeinterface/sorters/external/dartsort.py @@ -0,0 +1,104 @@ +from pathlib import Path + +import numpy as np + +from ..basesorter import BaseSorter +from ...core import load + + +class DartsortSorter(BaseSorter): + """Dartsort wrapper""" + + sorter_name = "dartsort" + requires_locations = False + compatible_with_parallel = {"loky": False, "multiprocessing": False, "threading": False} + sorter_description = """dartsort is a modular, drift-aware spike sorter developed in the Paninski lab. For installation and documentation, see https://dartsort.github.io""" + installation_mesg = """\nTo use dartsort run:\n + >>> pip install dartsort + + More information about installing dartsort at: + * https://dartsort.github.io + """ + + _default_params = {} + + _params_description = {} + + @classmethod + def _dynamic_params(cls): + from dartsort import DARTsortUserConfig + from pydantic import RootModel + + # the trick is to transform the DARTsortUserConfig (a pydantic.dataclass) into a pydantic model + Model = RootModel[DARTsortUserConfig] + # so we can dump to dict + cfg = Model(DARTsortUserConfig()) + default_params = cfg.model_dump(mode="python") + # and retrieve properties + schema = Model.model_json_schema() + default_params_descriptions = {} + for k, props in schema["$defs"]["DARTsortUserConfig"]["properties"].items(): + default_params_descriptions[k] = props["title"] + + return default_params, default_params_descriptions + + @classmethod + def is_installed(cls): + try: + import dartsort + + HAVE_DARTSORT = True + except ImportError: + HAVE_DARTSORT = False + + return HAVE_DARTSORT + + @staticmethod + def get_sorter_version(): + import dartsort + + if hasattr(dartsort, "__version__"): + return dartsort.__version__ + return "unknown" + + @classmethod + def _setup_recording(cls, recording, sorter_output_folder, params, verbose): + pass + + @classmethod + def _run_from_folder(cls, sorter_output_folder, params, verbose): + from dartsort import dartsort as dartsort_main + from dartsort import DARTsortUserConfig + + recording = cls.load_recording_from_folder(sorter_output_folder.parent, with_warnings=False) + + # Dartsort can be given the motion object optionaly + motion = params.pop("motion", None) + + # dartsort config are set using dataclass we need to map this + cfg = DARTsortUserConfig(**params) + + ret = dartsort_main( + recording, + sorter_output_folder, + cfg, + motion=motion, + ) + # the DARTsortSorting is not the spikeinterface sorting + dartsort_sorting = ret["sorting"] + sorting = dartsort_sorting.to_numpy_sorting() + + # Add main_channel_id property by taking mode of channels from spikes + labels = dartsort_sorting.labels + spike_channels = dartsort_sorting.channels + main_channel_indices = [np.bincount(spike_channels[labels == unit_id]).argmax() for unit_id in sorting.unit_ids] + main_channel_ids = recording.channel_ids[main_channel_indices] + sorting.set_property("main_channel_id", main_channel_ids) + # We save to the final_darsort_sorting folder to propagate the main_channel_id property + sorting.save(folder=sorter_output_folder / "final_darsort_sorting") + + @classmethod + def _get_result_from_folder(cls, sorter_output_folder): + sorter_output_folder = Path(sorter_output_folder) + sorting = load(sorter_output_folder / "final_darsort_sorting") + return sorting diff --git a/src/spikeinterface/sorters/external/tests/test_dartsort.py b/src/spikeinterface/sorters/external/tests/test_dartsort.py new file mode 100644 index 0000000000..32e0ca6c67 --- /dev/null +++ b/src/spikeinterface/sorters/external/tests/test_dartsort.py @@ -0,0 +1,19 @@ +import unittest +import pytest + +from spikeinterface.sorters import DartsortSorter +from spikeinterface.sorters.tests.common_tests import SorterCommonTestSuite + + +@pytest.mark.skipif(not DartsortSorter.is_installed(), reason="dartsort not installed") +class DartsortCommonTestSuite(SorterCommonTestSuite, unittest.TestCase): + SorterClass = DartsortSorter + + +if __name__ == "__main__": + from pathlib import Path + + test = DartsortCommonTestSuite() + test.cache_folder = Path(__file__).resolve().parents[4] / "cache_folder" / "sorters" + test.setUp() + test.test_with_run() diff --git a/src/spikeinterface/sorters/sorterlist.py b/src/spikeinterface/sorters/sorterlist.py index 7122fdf976..81279e8ebf 100644 --- a/src/spikeinterface/sorters/sorterlist.py +++ b/src/spikeinterface/sorters/sorterlist.py @@ -1,4 +1,5 @@ from .external.combinato import CombinatoSorter +from .external.dartsort import DartsortSorter from .external.hdsort import HDSortSorter from .external.herdingspikes import HerdingspikesSorter from .external.ironclust import IronClustSorter @@ -27,6 +28,7 @@ sorter_full_list = [ # external CombinatoSorter, + DartsortSorter, HDSortSorter, HerdingspikesSorter, IronClustSorter,