Skip to content

Commit 88d1c55

Browse files
authored
Merge pull request #3694 from alejoe91/analyzer-set-unit-property
Add `SortingAnalyzer.set_sorting_property()/get_sorting_property()` functions
2 parents dd0c6d3 + a36e1f6 commit 88d1c55

3 files changed

Lines changed: 130 additions & 19 deletions

File tree

src/spikeinterface/core/base.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def ids_to_indices(
145145
non_existent_ids = [id for id in ids if id not in self._main_ids]
146146
if non_existent_ids:
147147
error_msg = (
148-
f"IDs {non_existent_ids} are not channel ids of the extractor. \n"
148+
f"IDs {non_existent_ids} are not ids of the extractor. \n"
149149
f"Available ids are {self._main_ids} with dtype {self._main_ids.dtype}"
150150
)
151151
raise ValueError(error_msg)
@@ -231,10 +231,10 @@ def set_property(
231231
ids : list/np.array, default: None
232232
List of subset of ids to set the values, default: None
233233
if None which is the default all the ids are set or changed
234-
missing_value : object, default: None
234+
missing_value : Any, default: None
235235
In case the property is set on a subset of values ("ids" not None),
236-
it specifies the how the missing values should be filled.
237-
The missing_value has to be specified for types int and unsigned int.
236+
This argument specifies how to fill missing values.
237+
The `missing_value` is required for types int and unsigned int.
238238
"""
239239

240240
if values is None:
@@ -293,6 +293,11 @@ def set_property(
293293
), f"Mismatch between existing property dtype {existing_property.kind} and provided values dtype {dtype_kind}."
294294

295295
indices = self.ids_to_indices(ids)
296+
if dtype_kind == "U":
297+
# re-adjust the size of the property
298+
existing_unicode_size = max(len(s) for s in self._properties[key])
299+
new_unicode_size = max(max(len(s) for s in values), existing_unicode_size)
300+
self._properties[key] = self._properties[key].astype(f"<U{new_unicode_size}")
296301
self._properties[key][indices] = values
297302
else:
298303
indices = self.ids_to_indices(ids)

src/spikeinterface/core/sortinganalyzer.py

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from __future__ import annotations
2-
from typing import Literal, Optional
2+
from typing import Literal, Optional, Any
33

44
from pathlib import Path
55
from itertools import chain
@@ -223,13 +223,13 @@ class SortingAnalyzer:
223223

224224
def __init__(
225225
self,
226-
sorting=None,
227-
recording=None,
228-
rec_attributes=None,
229-
format=None,
230-
sparsity=None,
231-
return_scaled=True,
232-
backend_options=None,
226+
sorting: BaseSorting,
227+
recording: BaseRecording | None = None,
228+
rec_attributes: dict | None = None,
229+
format: str | None = None,
230+
sparsity: ChannelSparsity | None = None,
231+
return_scaled: bool = True,
232+
backend_options: dict | None = None,
233233
):
234234
# very fast init because checks are done in load and create
235235
self.sorting = sorting
@@ -239,6 +239,7 @@ def __init__(
239239
self.format = format
240240
self.sparsity = sparsity
241241
self.return_scaled = return_scaled
242+
self.folder: str | Path | None = None
242243

243244
# this is used to store temporary recording
244245
self._temporary_recording = None
@@ -742,6 +743,75 @@ def set_temporary_recording(self, recording: BaseRecording, check_dtype: bool =
742743
warnings.warn("SortingAnalyzer recording is already set. The current recording is temporarily replaced.")
743744
self._temporary_recording = recording
744745

746+
def set_sorting_property(
747+
self,
748+
key,
749+
values: list | np.ndarray | tuple,
750+
ids: list | np.ndarray | tuple | None = None,
751+
missing_value: Any = None,
752+
save: bool = True,
753+
) -> None:
754+
"""
755+
Set property vector for unit ids.
756+
757+
If the SortingAnalyzer backend is in memory, the property will be only set in memory.
758+
If the SortingAnalyzer backend is `binary_folder` or `zarr`, the property will also
759+
be saved to to the backend.
760+
761+
Parameters
762+
----------
763+
key : str
764+
The property name
765+
values : np.array
766+
Array of values for the property
767+
ids : list/np.array, default: None
768+
List of subset of ids to set the values.
769+
if None all the ids are set or changed
770+
missing_value : Any, default: None
771+
In case the property is set on a subset of values ("ids" not None),
772+
This argument specifies how to fill missing values.
773+
The `missing_value` is required for types int and unsigned int.
774+
save : bool, default: True
775+
If True, the property is saved to the backend if possible.
776+
"""
777+
self.sorting.set_property(key, values, ids=ids, missing_value=missing_value)
778+
if not self.is_read_only() and save:
779+
if self.format == "binary_folder":
780+
np.save(self.folder / "sorting" / "properties" / f"{key}.npy", self.sorting.get_property(key))
781+
elif self.format == "zarr":
782+
import zarr
783+
784+
zarr_root = self._get_zarr_root(mode="r+")
785+
prop_values = self.sorting.get_property(key)
786+
if prop_values.dtype.kind == "O":
787+
warnings.warn(f"Property {key} not saved because it is a python Object type")
788+
else:
789+
if key in zarr_root["sorting"]["properties"]:
790+
zarr_root["sorting"]["properties"][key][:] = prop_values
791+
else:
792+
zarr_root["sorting"]["properties"].create_dataset(name=key, data=prop_values, compressor=None)
793+
# IMPORTANT: we need to re-consolidate the zarr store!
794+
zarr.consolidate_metadata(zarr_root.store)
795+
796+
def get_sorting_property(self, key: str, ids: Optional[Iterable] = None) -> np.ndarray:
797+
"""
798+
Get property vector for unit ids.
799+
800+
Parameters
801+
----------
802+
key : str
803+
The property name
804+
ids : list/np.array, default: None
805+
List of subset of ids to get the values.
806+
if None all the ids are returned
807+
808+
Returns
809+
-------
810+
values : np.array
811+
Array of values for the property
812+
"""
813+
return self.sorting.get_property(key, ids=ids)
814+
745815
def _save_or_select_or_merge(
746816
self,
747817
format="binary_folder",
@@ -1177,6 +1247,9 @@ def get_sorting_provenance(self):
11771247
filename = self.folder / f"sorting_provenance.{type}"
11781248
sorting_provenance = None
11791249
if filename.exists():
1250+
# try-except here is because it's not required to be able
1251+
# to load the sorting provenance, as the user might have deleted
1252+
# the original sorting folder
11801253
try:
11811254
sorting_provenance = load(filename, base_folder=self.folder)
11821255
break
@@ -1186,11 +1259,16 @@ def get_sorting_provenance(self):
11861259

11871260
elif self.format == "zarr":
11881261
zarr_root = self._get_zarr_root(mode="r")
1262+
sorting_provenance = None
11891263
if "sorting_provenance" in zarr_root.keys():
1190-
sort_dict = zarr_root["sorting_provenance"][0]
1191-
sorting_provenance = load(sort_dict, base_folder=self.folder)
1192-
else:
1193-
sorting_provenance = None
1264+
# try-except here is because it's not required to be able
1265+
# to load the sorting provenance, as the user might have deleted
1266+
# the original sorting folder
1267+
try:
1268+
sort_dict = zarr_root["sorting_provenance"][0]
1269+
sorting_provenance = load(sort_dict, base_folder=self.folder)
1270+
except:
1271+
pass
11941272

11951273
return sorting_provenance
11961274

src/spikeinterface/core/tests/test_sortinganalyzer.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ def test_SortingAnalyzer_memory(tmp_path, dataset):
6666
)
6767
assert not sorting_analyzer.return_scaled
6868

69+
# test set_sorting_property
70+
sorting_analyzer.set_sorting_property(key="quality", values=["good"] * len(sorting_analyzer.unit_ids))
71+
sorting_analyzer.set_sorting_property(key="number", values=np.arange(len(sorting_analyzer.unit_ids)))
72+
assert "quality" in sorting_analyzer.sorting.get_property_keys()
73+
assert "number" in sorting_analyzer.sorting.get_property_keys()
74+
6975

7076
def test_SortingAnalyzer_binary_folder(tmp_path, dataset):
7177
recording, sorting = dataset
@@ -103,6 +109,15 @@ def test_SortingAnalyzer_binary_folder(tmp_path, dataset):
103109
assert not sorting_analyzer.return_scaled
104110
_check_sorting_analyzers(sorting_analyzer, sorting, cache_folder=tmp_path)
105111

112+
# test set_sorting_property
113+
sorting_analyzer.set_sorting_property(key="quality", values=["good"] * len(sorting_analyzer.unit_ids))
114+
sorting_analyzer.set_sorting_property(key="number", values=np.arange(len(sorting_analyzer.unit_ids)))
115+
assert "quality" in sorting_analyzer.sorting.get_property_keys()
116+
assert "number" in sorting_analyzer.sorting.get_property_keys()
117+
sorting_analyzer_reloded = load_sorting_analyzer(folder, format="auto")
118+
assert "quality" in sorting_analyzer_reloded.sorting.get_property_keys()
119+
assert "number" in sorting_analyzer.sorting.get_property_keys()
120+
106121

107122
def test_SortingAnalyzer_zarr(tmp_path, dataset):
108123
recording, sorting = dataset
@@ -176,6 +191,15 @@ def test_SortingAnalyzer_zarr(tmp_path, dataset):
176191
== LZMA.codec_id
177192
)
178193

194+
# test set_sorting_property
195+
sorting_analyzer.set_sorting_property(key="quality", values=["good"] * len(sorting_analyzer.unit_ids))
196+
sorting_analyzer.set_sorting_property(key="number", values=np.arange(len(sorting_analyzer.unit_ids)))
197+
assert "quality" in sorting_analyzer.sorting.get_property_keys()
198+
assert "number" in sorting_analyzer.sorting.get_property_keys()
199+
sorting_analyzer_reloded = load_sorting_analyzer(sorting_analyzer.folder, format="auto")
200+
assert "quality" in sorting_analyzer_reloded.sorting.get_property_keys()
201+
assert "number" in sorting_analyzer.sorting.get_property_keys()
202+
179203

180204
def test_load_without_runtime_info(tmp_path, dataset):
181205
import zarr
@@ -262,9 +286,6 @@ def _check_sorting_analyzers(sorting_analyzer, original_sorting, cache_folder):
262286
assert "sampling_frequency" in sorting_analyzer.rec_attributes
263287
assert "num_samples" in sorting_analyzer.rec_attributes
264288

265-
probe = sorting_analyzer.get_probe()
266-
sparsity = sorting_analyzer.sparsity
267-
268289
# compute
269290
sorting_analyzer.compute("dummy", param1=5.5)
270291
# equivalent
@@ -367,6 +388,9 @@ def _check_sorting_analyzers(sorting_analyzer, original_sorting, cache_folder):
367388
else:
368389
folder = None
369390
sorting_analyzer4 = sorting_analyzer.merge_units(merge_unit_groups=[[0, 1]], format=format, folder=folder)
391+
assert 0 not in sorting_analyzer4.unit_ids
392+
assert 1 not in sorting_analyzer4.unit_ids
393+
assert len(sorting_analyzer4.unit_ids) == len(sorting_analyzer.unit_ids) - 1
370394

371395
if format != "memory":
372396
if format == "zarr":
@@ -380,6 +404,10 @@ def _check_sorting_analyzers(sorting_analyzer, original_sorting, cache_folder):
380404
sorting_analyzer5 = sorting_analyzer.merge_units(
381405
merge_unit_groups=[[0, 1]], new_unit_ids=[50], format=format, folder=folder, merging_mode="hard"
382406
)
407+
assert 0 not in sorting_analyzer5.unit_ids
408+
assert 1 not in sorting_analyzer5.unit_ids
409+
assert len(sorting_analyzer5.unit_ids) == len(sorting_analyzer.unit_ids) - 1
410+
assert 50 in sorting_analyzer5.unit_ids
383411

384412
# test compute with extension-specific params
385413
sorting_analyzer.compute(["dummy"], extension_params={"dummy": {"param1": 5.5}})

0 commit comments

Comments
 (0)