Repository: SpikeInterface/spikeinterface
Pull request #3406 (2024-09-13 09:51:46)

Sortingview: only round float properties
Recently we added a `format_float_positional` for sortingview properties, but this must be applied only to floats


Patch:
diff --git a/doc/development/development.rst b/doc/development/development.rst
index 246a2bcb9..1094b466f 100644
--- a/doc/development/development.rst
+++ b/doc/development/development.rst
@@ -189,17 +189,9 @@ so that the user knows what the options are.
 Miscelleaneous Stylistic Conventions
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-#. Avoid using abbreviations in variable names (e.g. use :code:`recording` instead of :code:`rec`). It is especially important to avoid single letter variables.
+#. Avoid using abreviations in variable names (e.g., use :code:`recording` instead of :code:`rec`). It is especially important to avoid single letter variables.
 #. Use index as singular and indices for plural following the NumPy convention. Avoid idx or indexes. Plus, id and ids are reserved for identifiers (i.e. channel_ids)
 #. We use file_path and folder_path (instead of file_name and folder_name) for clarity.
-#. For creating headers to divide sections of code we use the following convention (see issue `#3019 <https://github.com/SpikeInterface/spikeinterface/issues/3019>`_):
-
-
-.. code:: python
-
-    #########################################
-    # A header
-    #########################################
 
 
 How to build the documentation
diff --git a/src/spikeinterface/core/sortinganalyzer.py b/src/spikeinterface/core/sortinganalyzer.py
index 424fab7c5..49a31738e 100644
--- a/src/spikeinterface/core/sortinganalyzer.py
+++ b/src/spikeinterface/core/sortinganalyzer.py
@@ -77,9 +77,6 @@ def create_sorting_analyzer(
     return_scaled : bool, default: True
         All extensions that play with traces will use this global return_scaled : "waveforms", "noise_levels", "templates".
         This prevent return_scaled being differents from different extensions and having wrong snr for instance.
-    overwrite: bool, default: False
-        If True, overwrite the folder if it already exists.
-
 
     Returns
     -------
@@ -489,11 +486,7 @@ class SortingAnalyzer:
 
         if is_path_remote(str(self.folder)):
             mode = "r"
-        # we open_consolidated only if we are in read mode
-        if mode in ("r+", "a"):
-            zarr_root = zarr.open(str(self.folder), mode=mode, storage_options=self.storage_options)
-        else:
-            zarr_root = zarr.open_consolidated(self.folder, mode=mode, storage_options=self.storage_options)
+        zarr_root = zarr.open(self.folder, mode=mode, storage_options=self.storage_options)
         return zarr_root
 
     @classmethod
@@ -571,13 +564,11 @@ class SortingAnalyzer:
 
         recording_info = zarr_root.create_group("extensions")
 
-        zarr.consolidate_metadata(zarr_root.store)
-
     @classmethod
     def load_from_zarr(cls, folder, recording=None, storage_options=None):
         import zarr
 
-        zarr_root = zarr.open_consolidated(str(folder), mode="r", storage_options=storage_options)
+        zarr_root = zarr.open(str(folder), mode="r", storage_options=storage_options)
 
         # load internal sorting in memory
         sorting = NumpySorting.from_sorting(
@@ -623,7 +614,6 @@ class SortingAnalyzer:
             format="zarr",
             sparsity=sparsity,
             return_scaled=return_scaled,
-            storage_options=storage_options,
         )
 
         return sorting_analyzer
@@ -1472,7 +1462,7 @@ extension_params={"waveforms":{"ms_before":1.5, "ms_after": "2.5"}}\
         if self.format != "memory" and self.has_extension(extension_name):
             # need a reload to reset the folder
             ext = self.load_extension(extension_name)
-            ext.delete()
+            ext.reset()
 
         # remove from dict
         self.extensions.pop(extension_name, None)
@@ -2014,17 +2004,19 @@ class AnalyzerExtension:
             # NB: this call to _save_params() also resets the folder or zarr group
             self._save_params()
             self._save_importing_provenance()
+            self._save_run_info()
 
         t_start = perf_counter()
         self._run(**kwargs)
         t_end = perf_counter()
         self.run_info["runtime_s"] = t_end - t_start
-        self.run_info["run_completed"] = True
 
         if save and not self.sorting_analyzer.is_read_only():
-            self._save_run_info()
             self._save_data(**kwargs)
 
+        self.run_info["run_completed"] = True
+        self._save_run_info()
+
     def save(self, **kwargs):
         self._save_params()
         self._save_importing_provenance()
@@ -2070,7 +2062,7 @@ class AnalyzerExtension:
                     except:
                         raise Exception(f"Could not save {ext_data_name} as extension data")
         elif self.format == "zarr":
-            import zarr
+
             import numcodecs
 
             extension_group = self._get_zarr_extension_group(mode="r+")
@@ -2104,8 +2096,6 @@ class AnalyzerExtension:
                     except:
                         raise Exception(f"Could not save {ext_data_name} as extension data")
                     extension_group[ext_data_name].attrs["object"] = True
-            # we need to re-consolidate
-            zarr.consolidate_metadata(self.sorting_analyzer._get_zarr_root().store)
 
     def _reset_extension_folder(self):
         """
@@ -2120,35 +2110,8 @@ class AnalyzerExtension:
         elif self.format == "zarr":
             import zarr
 
-            zarr_root = self.sorting_analyzer._get_zarr_root(mode="r+")
-            _ = zarr_root["extensions"].create_group(self.extension_name, overwrite=True)
-            zarr.consolidate_metadata(zarr_root.store)
-
-    def _delete_extension_folder(self):
-        """
-        Delete the extension in a folder (binary or zarr).
-        """
-        if self.format == "binary_folder":
-            extension_folder = self._get_binary_extension_folder()
-            if extension_folder.is_dir():
-                shutil.rmtree(extension_folder)
-
-        elif self.format == "zarr":
-            import zarr
-
-            zarr_root = self.sorting_analyzer._get_zarr_root(mode="r+")
-            if self.extension_name in zarr_root["extensions"]:
-                del zarr_root["extensions"][self.extension_name]
-                zarr.consolidate_metadata(zarr_root.store)
-
-    def delete(self):
-        """
-        Delete the extension from the folder or zarr and from the dict.
-        """
-        self._delete_extension_folder()
-        self.params = None
-        self.run_info = self._default_run_info_dict()
-        self.data = dict()
+            zarr_root = zarr.open(self.folder, mode="r+")
+            extension_group = zarr_root["extensions"].create_group(self.extension_name, overwrite=True)
 
     def reset(self):
         """
@@ -2165,7 +2128,7 @@ class AnalyzerExtension:
         Set parameters for the extension and
         make it persistent in json.
         """
-        # this ensure data is also deleted and corresponds to params
+        # this ensure data is also deleted and corresponf to params
         # this also ensure the group is created
         self._reset_extension_folder()
 
@@ -2178,6 +2141,7 @@ class AnalyzerExtension:
         if save:
             self._save_params()
             self._save_importing_provenance()
+            self._save_run_info()
 
     def _save_params(self):
         params_to_save = self.params.copy()
diff --git a/src/spikeinterface/widgets/unit_summary.py b/src/spikeinterface/widgets/unit_summary.py
index 755e60ccb..0b2a348ed 100644
--- a/src/spikeinterface/widgets/unit_summary.py
+++ b/src/spikeinterface/widgets/unit_summary.py
@@ -1,5 +1,4 @@
 from __future__ import annotations
-from collections import defaultdict
 
 import numpy as np
 
@@ -18,7 +17,7 @@ class UnitSummaryWidget(BaseWidget):
     """
     Plot a unit summary.
 
-    If amplitudes are alreday computed, they are displayed.
+    If amplitudes are alreday computed they are displayed.
 
     Parameters
     ----------
@@ -31,14 +30,6 @@ class UnitSummaryWidget(BaseWidget):
     sparsity : ChannelSparsity or None, default: None
         Optional ChannelSparsity to apply.
         If SortingAnalyzer is already sparse, the argument is ignored
-    subwidget_kwargs : dict or None, default: None
-        Parameters for the subwidgets in a nested dictionary
-            unit_locations : UnitLocationsWidget (see UnitLocationsWidget for details)
-            unit_waveforms : UnitWaveformsWidget (see UnitWaveformsWidget for details)
-            unit_waveform_density_map : UnitWaveformDensityMapWidget (see UnitWaveformDensityMapWidget for details)
-            autocorrelograms : AutoCorrelogramsWidget (see AutoCorrelogramsWidget for details)
-            amplitudes : AmplitudesWidget (see AmplitudesWidget for details)
-        Please note that the unit_colors should not be set in subwidget_kwargs, but directly as a parameter of plot_unit_summary.
     """
 
     # possible_backends = {}
@@ -49,29 +40,21 @@ class UnitSummaryWidget(BaseWidget):
         unit_id,
         unit_colors=None,
         sparsity=None,
-        subwidget_kwargs=None,
+        radius_um=100,
         backend=None,
         **backend_kwargs,
     ):
+
         sorting_analyzer = self.ensure_sorting_analyzer(sorting_analyzer)
 
         if unit_colors is None:
             unit_colors = get_unit_colors(sorting_analyzer)
 
-        if subwidget_kwargs is None:
-            subwidget_kwargs = dict()
-        for kwargs in subwidget_kwargs.values():
-            if "unit_colors" in kwargs:
-                raise ValueError(
-                    "unit_colors should not be set in subwidget_kwargs, but directly as a parameter of plot_unit_summary"
-                )
-
         plot_data = dict(
             sorting_analyzer=sorting_analyzer,
             unit_id=unit_id,
             unit_colors=unit_colors,
             sparsity=sparsity,
-            subwidget_kwargs=subwidget_kwargs,
         )
 
         BaseWidget.__init__(self, plot_data, backend=backend, **backend_kwargs)
@@ -87,14 +70,6 @@ class UnitSummaryWidget(BaseWidget):
         unit_colors = dp.unit_colors
         sparsity = dp.sparsity
 
-        # defaultdict returns empty dict if key not found in subwidget_kwargs
-        subwidget_kwargs = defaultdict(lambda: dict(), dp.subwidget_kwargs)
-        unitlocationswidget_kwargs = subwidget_kwargs["unit_locations"]
-        unitwaveformswidget_kwargs = subwidget_kwargs["unit_waveforms"]
-        unitwaveformdensitymapwidget_kwargs = subwidget_kwargs["unit_waveform_density_map"]
-        autocorrelogramswidget_kwargs = subwidget_kwargs["autocorrelograms"]
-        amplitudeswidget_kwargs = subwidget_kwargs["amplitudes"]
-
         # force the figure without axes
         if "figsize" not in backend_kwargs:
             backend_kwargs["figsize"] = (18, 7)
@@ -124,7 +99,6 @@ class UnitSummaryWidget(BaseWidget):
                 plot_legend=False,
                 backend="matplotlib",
                 ax=ax1,
-                **unitlocationswidget_kwargs,
             )
 
             unit_locations = sorting_analyzer.get_extension("unit_locations").get_data(outputs="by_unit")
@@ -147,7 +121,6 @@ class UnitSummaryWidget(BaseWidget):
             sparsity=sparsity,
             backend="matplotlib",
             ax=ax2,
-            **unitwaveformswidget_kwargs,
         )
 
         ax2.set_title(None)
@@ -161,7 +134,6 @@ class UnitSummaryWidget(BaseWidget):
             same_axis=False,
             backend="matplotlib",
             ax=ax3,
-            **unitwaveformdensitymapwidget_kwargs,
         )
         ax3.set_ylabel(None)
 
@@ -173,7 +145,6 @@ class UnitSummaryWidget(BaseWidget):
                 unit_colors=unit_colors,
                 backend="matplotlib",
                 ax=ax4,
-                **autocorrelogramswidget_kwargs,
             )
 
             ax4.set_title(None)
@@ -191,7 +162,6 @@ class UnitSummaryWidget(BaseWidget):
                 plot_histograms=True,
                 backend="matplotlib",
                 axes=axes,
-                **amplitudeswidget_kwargs,
             )
 
         fig.suptitle(f"unit_id: {dp.unit_id}")
diff --git a/src/spikeinterface/widgets/utils_sortingview.py b/src/spikeinterface/widgets/utils_sortingview.py
index 7a9dc4782..d18c581b6 100644
--- a/src/spikeinterface/widgets/utils_sortingview.py
+++ b/src/spikeinterface/widgets/utils_sortingview.py
@@ -141,12 +141,13 @@ def generate_unit_table_view(
                 elif prop_name in tm_props:
                     property_values = tm_data[prop_name].values
 
-                # Check for NaN values
+                # Check for NaN values and round floats
                 val0 = np.array(property_values[0])
                 if val0.dtype.kind == "f":
                     if np.isnan(property_values[ui]):
                         continue
-                values[prop_name] = np.format_float_positional(property_values[ui], precision=4, fractional=False)
+                    property_values[ui] = np.format_float_positional(property_values[ui], precision=4, fractional=False)
+                values[prop_name] = property_values[ui]
             ut_rows.append(vv.UnitsTableRow(unit_id=unit, values=check_json(values)))
 
     v_units_table = vv.UnitsTable(rows=ut_rows, columns=ut_columns, similarity_scores=similarity_scores)


Test patch:
diff --git a/src/spikeinterface/core/tests/test_analyzer_extension_core.py b/src/spikeinterface/core/tests/test_analyzer_extension_core.py
index 626899ab6..b4d96a339 100644
--- a/src/spikeinterface/core/tests/test_analyzer_extension_core.py
+++ b/src/spikeinterface/core/tests/test_analyzer_extension_core.py
@@ -79,20 +79,15 @@ def _check_result_extension(sorting_analyzer, extension_name, cache_folder):
 )
 def test_ComputeRandomSpikes(format, sparse, create_cache_folder):
     cache_folder = create_cache_folder
-    print("Creating analyzer")
     sorting_analyzer = get_sorting_analyzer(cache_folder, format=format, sparse=sparse)
 
-    print("Computing random spikes")
     ext = sorting_analyzer.compute("random_spikes", max_spikes_per_unit=10, seed=2205)
     indices = ext.data["random_spikes_indices"]
     assert indices.size == 10 * sorting_analyzer.sorting.unit_ids.size
 
-    print("Checking results")
     _check_result_extension(sorting_analyzer, "random_spikes", cache_folder)
-    print("Delering extension")
     sorting_analyzer.delete_extension("random_spikes")
 
-    print("Re-computing random spikes")
     ext = sorting_analyzer.compute("random_spikes", method="all")
     indices = ext.data["random_spikes_indices"]
     assert indices.size == len(sorting_analyzer.sorting.to_spike_vector())
diff --git a/src/spikeinterface/core/tests/test_sortinganalyzer.py b/src/spikeinterface/core/tests/test_sortinganalyzer.py
index 77b8f2c5b..3f45487f4 100644
--- a/src/spikeinterface/core/tests/test_sortinganalyzer.py
+++ b/src/spikeinterface/core/tests/test_sortinganalyzer.py
@@ -126,8 +126,6 @@ def test_SortingAnalyzer_zarr(tmp_path, dataset):
 
 
 def test_load_without_runtime_info(tmp_path, dataset):
-    import zarr
-
     recording, sorting = dataset
 
     folder = tmp_path / "test_SortingAnalyzer_run_info"
@@ -155,7 +153,6 @@ def test_load_without_runtime_info(tmp_path, dataset):
     root = sorting_analyzer._get_zarr_root(mode="r+")
     for ext in extensions:
         del root["extensions"][ext].attrs["run_info"]
-        zarr.consolidate_metadata(root.store)
     # should raise a warning for missing run_info
     with pytest.warns(UserWarning):
         sorting_analyzer = load_sorting_analyzer(folder, format="auto")
diff --git a/src/spikeinterface/qualitymetrics/tests/conftest.py b/src/spikeinterface/qualitymetrics/tests/conftest.py
index 01fa16c8d..bb2a34534 100644
--- a/src/spikeinterface/qualitymetrics/tests/conftest.py
+++ b/src/spikeinterface/qualitymetrics/tests/conftest.py
@@ -5,11 +5,8 @@ from spikeinterface.core import (
     create_sorting_analyzer,
 )
 
-job_kwargs = dict(n_jobs=2, progress_bar=True, chunk_duration="1s")
 
-
-@pytest.fixture(scope="module")
-def small_sorting_analyzer():
+def _small_sorting_analyzer():
     recording, sorting = generate_ground_truth_recording(
         durations=[2.0],
         num_units=10,
@@ -36,36 +33,5 @@ def small_sorting_analyzer():
 
 
 @pytest.fixture(scope="module")
-def sorting_analyzer_simple():
-    # we need high firing rate for amplitude_cutoff
-    recording, sorting = generate_ground_truth_recording(
-        durations=[
-            120.0,
-        ],
-        sampling_frequency=30_000.0,
-        num_channels=6,
-        num_units=10,
-        generate_sorting_kwargs=dict(firing_rates=10.0, refractory_period_ms=4.0),
-        generate_unit_locations_kwargs=dict(
-            margin_um=5.0,
-            minimum_z=5.0,
-            maximum_z=20.0,
-        ),
-        generate_templates_kwargs=dict(
-            unit_params=dict(
-                alpha=(200.0, 500.0),
-            )
-        ),
-        noise_kwargs=dict(noise_levels=5.0, strategy="tile_pregenerated"),
-        seed=1205,
-    )
-
-    sorting_analyzer = create_sorting_analyzer(sorting, recording, format="memory", sparse=True)
-
-    sorting_analyzer.compute("random_spikes", max_spikes_per_unit=300, seed=1205)
-    sorting_analyzer.compute("noise_levels")
-    sorting_analyzer.compute("waveforms", **job_kwargs)
-    sorting_analyzer.compute("templates")
-    sorting_analyzer.compute("spike_amplitudes", **job_kwargs)
-
-    return sorting_analyzer
+def small_sorting_analyzer():
+    return _small_sorting_analyzer()
diff --git a/src/spikeinterface/qualitymetrics/tests/test_metrics_functions.py b/src/spikeinterface/qualitymetrics/tests/test_metrics_functions.py
index e7fc7ce20..bb222200e 100644
--- a/src/spikeinterface/qualitymetrics/tests/test_metrics_functions.py
+++ b/src/spikeinterface/qualitymetrics/tests/test_metrics_functions.py
@@ -135,6 +135,36 @@ def test_unit_id_order_independence(small_sorting_analyzer):
         assert quality_metrics_2[metric][1] == metric_1_data["#4"]
 
 
+def _sorting_analyzer_simple():
+    recording, sorting = generate_ground_truth_recording(
+        durations=[
+            50.0,
+        ],
+        sampling_frequency=30_000.0,
+        num_channels=6,
+        num_units=10,
+        generate_sorting_kwargs=dict(firing_rates=6.0, refractory_period_ms=4.0),
+        noise_kwargs=dict(noise_levels=5.0, strategy="tile_pregenerated"),
+        seed=2205,
+    )
+
+    sorting_analyzer = create_sorting_analyzer(sorting, recording, format="memory", sparse=True)
+
+    sorting_analyzer.compute("random_spikes", max_spikes_per_unit=300, seed=2205)
+    sorting_analyzer.compute("noise_levels")
+    sorting_analyzer.compute("waveforms", **job_kwargs)
+    sorting_analyzer.compute("templates")
+    sorting_analyzer.compute("principal_components", n_components=5, mode="by_channel_local", **job_kwargs)
+    sorting_analyzer.compute("spike_amplitudes", **job_kwargs)
+
+    return sorting_analyzer
+
+
+@pytest.fixture(scope="module")
+def sorting_analyzer_simple():
+    return _sorting_analyzer_simple()
+
+
 def _sorting_violation():
     max_time = 100.0
     sampling_frequency = 30000
@@ -546,7 +576,6 @@ if __name__ == "__main__":
     test_unit_structure_in_output(_small_sorting_analyzer())
 
     # test_calculate_firing_rate_num_spikes(sorting_analyzer)
-
     # test_calculate_snrs(sorting_analyzer)
     # test_calculate_amplitude_cutoff(sorting_analyzer)
     # test_calculate_presence_ratio(sorting_analyzer)
diff --git a/src/spikeinterface/qualitymetrics/tests/test_quality_metric_calculator.py b/src/spikeinterface/qualitymetrics/tests/test_quality_metric_calculator.py
index a6415c58e..28869ba5f 100644
--- a/src/spikeinterface/qualitymetrics/tests/test_quality_metric_calculator.py
+++ b/src/spikeinterface/qualitymetrics/tests/test_quality_metric_calculator.py
@@ -2,6 +2,7 @@ import pytest
 from pathlib import Path
 import numpy as np
 
+
 from spikeinterface.core import (
     generate_ground_truth_recording,
     create_sorting_analyzer,
@@ -14,11 +15,54 @@ from spikeinterface.qualitymetrics import (
     compute_quality_metrics,
 )
 
+
 job_kwargs = dict(n_jobs=2, progress_bar=True, chunk_duration="1s")
 
 
+def get_sorting_analyzer(seed=2205):
+    # we need high firing rate for amplitude_cutoff
+    recording, sorting = generate_ground_truth_recording(
+        durations=[
+            120.0,
+        ],
+        sampling_frequency=30_000.0,
+        num_channels=6,
+        num_units=10,
+        generate_sorting_kwargs=dict(firing_rates=10.0, refractory_period_ms=4.0),
+        generate_unit_locations_kwargs=dict(
+            margin_um=5.0,
+            minimum_z=5.0,
+            maximum_z=20.0,
+        ),
+        generate_templates_kwargs=dict(
+            unit_params=dict(
+                alpha=(200.0, 500.0),
+            )
+        ),
+        noise_kwargs=dict(noise_levels=5.0, strategy="tile_pregenerated"),
+        seed=seed,
+    )
+
+    sorting_analyzer = create_sorting_analyzer(sorting, recording, format="memory", sparse=True)
+
+    sorting_analyzer.compute("random_spikes", max_spikes_per_unit=300, seed=seed)
+    sorting_analyzer.compute("noise_levels")
+    sorting_analyzer.compute("waveforms", **job_kwargs)
+    sorting_analyzer.compute("templates")
+    sorting_analyzer.compute("spike_amplitudes", **job_kwargs)
+
+    return sorting_analyzer
+
+
+@pytest.fixture(scope="module")
+def sorting_analyzer_simple():
+    sorting_analyzer = get_sorting_analyzer(seed=2205)
+    return sorting_analyzer
+
+
 def test_compute_quality_metrics(sorting_analyzer_simple):
     sorting_analyzer = sorting_analyzer_simple
+    print(sorting_analyzer)
 
     # without PCs
     metrics = compute_quality_metrics(