Skip to content

Commit 238e694

Browse files
authored
Merge branch 'main' into curation-pot-merges
2 parents b1b6c22 + 161efc4 commit 238e694

8 files changed

Lines changed: 110 additions & 32 deletions

File tree

src/spikeinterface/comparison/paircomparisons.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -456,12 +456,12 @@ def print_summary(self, well_detected_score=None, redundant_score=None, overmerg
456456
num_gt=len(self.unit1_ids),
457457
num_tested=len(self.unit2_ids),
458458
num_well_detected=self.count_well_detected_units(well_detected_score),
459-
num_redundant=self.count_redundant_units(redundant_score),
460-
num_overmerged=self.count_overmerged_units(overmerged_score),
461459
)
462460

463461
if self.exhaustive_gt:
464462
txt = txt + _template_summary_part2
463+
d["num_redundant"] = self.count_redundant_units(redundant_score)
464+
d["num_overmerged"] = self.count_overmerged_units(overmerged_score)
465465
d["num_false_positive_units"] = self.count_false_positive_units()
466466
d["num_bad"] = self.count_bad_units()
467467

@@ -676,11 +676,11 @@ def count_units_categories(
676676
GT num_units: {num_gt}
677677
TESTED num_units: {num_tested}
678678
num_well_detected: {num_well_detected}
679-
num_redundant: {num_redundant}
680-
num_overmerged: {num_overmerged}
681679
"""
682680

683-
_template_summary_part2 = """num_false_positive_units {num_false_positive_units}
681+
_template_summary_part2 = """num_redundant: {num_redundant}
682+
num_overmerged: {num_overmerged}
683+
num_false_positive_units {num_false_positive_units}
684684
num_bad: {num_bad}
685685
"""
686686

src/spikeinterface/core/base.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .globals import get_global_tmp_folder, is_set_global_tmp_folder
1818
from .core_tools import (
1919
check_json,
20+
clean_zarr_folder_name,
2021
is_dict_extractor,
2122
SIJsonEncoder,
2223
make_paths_relative,
@@ -1061,9 +1062,7 @@ def save_to_zarr(
10611062
print(f"Use zarr_path={zarr_path}")
10621063
else:
10631064
if storage_options is None:
1064-
folder = Path(folder)
1065-
if folder.suffix != ".zarr":
1066-
folder = folder.parent / f"{folder.stem}.zarr"
1065+
folder = clean_zarr_folder_name(folder)
10671066
if folder.is_dir() and overwrite:
10681067
shutil.rmtree(folder)
10691068
zarr_path = folder

src/spikeinterface/core/core_tools.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ def check_json(dictionary: dict) -> dict:
153153
return json.loads(json_string)
154154

155155

156+
def clean_zarr_folder_name(folder):
157+
folder = Path(folder)
158+
if folder.suffix != ".zarr":
159+
folder = folder.parent / f"{folder.stem}.zarr"
160+
return folder
161+
162+
156163
def add_suffix(file_path, possible_suffix):
157164
file_path = Path(file_path)
158165
if isinstance(possible_suffix, str):

src/spikeinterface/core/recording_tools.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -929,7 +929,9 @@ def get_rec_attributes(recording):
929929
return rec_attributes
930930

931931

932-
def do_recording_attributes_match(recording1, recording2_attributes) -> bool:
932+
def do_recording_attributes_match(
933+
recording1: "BaseRecording", recording2_attributes: bool, check_dtype: bool = True
934+
) -> tuple[bool, str]:
933935
"""
934936
Check if two recordings have the same attributes
935937
@@ -939,22 +941,43 @@ def do_recording_attributes_match(recording1, recording2_attributes) -> bool:
939941
The first recording object
940942
recording2_attributes : dict
941943
The recording attributes to test against
944+
check_dtype : bool, default: True
945+
If True, check if the recordings have the same dtype
942946
943947
Returns
944948
-------
945949
bool
946950
True if the recordings have the same attributes
951+
str
952+
A string with the exception message with the attributes that do not match
947953
"""
948954
recording1_attributes = get_rec_attributes(recording1)
949955
recording2_attributes = deepcopy(recording2_attributes)
950956
recording1_attributes.pop("properties")
951957
recording2_attributes.pop("properties")
952958

953-
return (
954-
np.array_equal(recording1_attributes["channel_ids"], recording2_attributes["channel_ids"])
955-
and recording1_attributes["sampling_frequency"] == recording2_attributes["sampling_frequency"]
956-
and recording1_attributes["num_channels"] == recording2_attributes["num_channels"]
957-
and recording1_attributes["num_samples"] == recording2_attributes["num_samples"]
958-
and recording1_attributes["is_filtered"] == recording2_attributes["is_filtered"]
959-
and recording1_attributes["dtype"] == recording2_attributes["dtype"]
960-
)
959+
attributes_match = True
960+
non_matching_attrs = []
961+
962+
if not np.array_equal(recording1_attributes["channel_ids"], recording2_attributes["channel_ids"]):
963+
non_matching_attrs.append("channel_ids")
964+
if not recording1_attributes["sampling_frequency"] == recording2_attributes["sampling_frequency"]:
965+
non_matching_attrs.append("sampling_frequency")
966+
if not recording1_attributes["num_channels"] == recording2_attributes["num_channels"]:
967+
non_matching_attrs.append("num_channels")
968+
if not recording1_attributes["num_samples"] == recording2_attributes["num_samples"]:
969+
non_matching_attrs.append("num_samples")
970+
# dtype is optional
971+
if "dtype" in recording1_attributes and "dtype" in recording2_attributes:
972+
if check_dtype:
973+
if not recording1_attributes["dtype"] == recording2_attributes["dtype"]:
974+
non_matching_attrs.append("dtype")
975+
976+
if len(non_matching_attrs) > 0:
977+
attributes_match = False
978+
exception_str = f"Recordings do not match in the following attributes: {non_matching_attrs}"
979+
else:
980+
attributes_match = True
981+
exception_str = ""
982+
983+
return attributes_match, exception_str

src/spikeinterface/core/sortinganalyzer.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from .base import load_extractor
2525
from .recording_tools import check_probe_do_not_overlap, get_rec_attributes, do_recording_attributes_match
26-
from .core_tools import check_json, retrieve_importing_provenance, is_path_remote
26+
from .core_tools import check_json, retrieve_importing_provenance, is_path_remote, clean_zarr_folder_name
2727
from .sorting_tools import generate_unit_ids_for_merge_group, _get_ids_after_merging
2828
from .job_tools import split_job_kwargs
2929
from .numpyextractors import NumpySorting
@@ -111,6 +111,8 @@ def create_sorting_analyzer(
111111
sparsity off (or give external sparsity) like this.
112112
"""
113113
if format != "memory":
114+
if format == "zarr":
115+
folder = clean_zarr_folder_name(folder)
114116
if Path(folder).is_dir():
115117
if not overwrite:
116118
raise ValueError(f"Folder already exists {folder}! Use overwrite=True to overwrite it.")
@@ -162,6 +164,8 @@ def load_sorting_analyzer(folder, load_extensions=True, format="auto"):
162164
The loaded SortingAnalyzer
163165
164166
"""
167+
if format == "zarr":
168+
folder = clean_zarr_folder_name(folder)
165169
return SortingAnalyzer.load(folder, load_extensions=load_extensions, format=format)
166170

167171

@@ -269,6 +273,8 @@ def create(
269273
sorting_analyzer = cls.load_from_binary_folder(folder, recording=recording)
270274
sorting_analyzer.folder = Path(folder)
271275
elif format == "zarr":
276+
assert folder is not None, "For format='zarr' folder must be provided"
277+
folder = clean_zarr_folder_name(folder)
272278
cls.create_zarr(folder, sorting, recording, sparsity, return_scaled, rec_attributes=None)
273279
sorting_analyzer = cls.load_from_zarr(folder, recording=recording)
274280
sorting_analyzer.folder = Path(folder)
@@ -487,10 +493,7 @@ def create_zarr(cls, folder, sorting, recording, sparsity, return_scaled, rec_at
487493
import zarr
488494
import numcodecs
489495

490-
folder = Path(folder)
491-
# force zarr sufix
492-
if folder.suffix != ".zarr":
493-
folder = folder.parent / f"{folder.stem}.zarr"
496+
folder = clean_zarr_folder_name(folder)
494497

495498
if folder.is_dir():
496499
raise ValueError(f"Folder already exists {folder}")
@@ -613,7 +616,7 @@ def load_from_zarr(cls, folder, recording=None, storage_options=None):
613616

614617
return sorting_analyzer
615618

616-
def set_temporary_recording(self, recording: BaseRecording):
619+
def set_temporary_recording(self, recording: BaseRecording, check_dtype: bool = True):
617620
"""
618621
Sets a temporary recording object. This function can be useful to temporarily set
619622
a "cached" recording object that is not saved in the SortingAnalyzer object to speed up
@@ -625,12 +628,17 @@ def set_temporary_recording(self, recording: BaseRecording):
625628
----------
626629
recording : BaseRecording
627630
The recording object to set as temporary recording.
631+
check_dtype : bool, default: True
632+
If True, check that the dtype of the temporary recording is the same as the original recording.
628633
"""
629634
# check that recording is compatible
630-
assert do_recording_attributes_match(recording, self.rec_attributes), "Recording attributes do not match."
631-
assert np.array_equal(
632-
recording.get_channel_locations(), self.get_channel_locations()
633-
), "Recording channel locations do not match."
635+
attributes_match, exception_str = do_recording_attributes_match(
636+
recording, self.rec_attributes, check_dtype=check_dtype
637+
)
638+
if not attributes_match:
639+
raise ValueError(exception_str)
640+
if not np.array_equal(recording.get_channel_locations(), self.get_channel_locations()):
641+
raise ValueError("Recording channel locations do not match.")
634642
if self._recording is not None:
635643
warnings.warn("SortingAnalyzer recording is already set. The current recording is temporarily replaced.")
636644
self._temporary_recording = recording
@@ -768,9 +776,7 @@ def _save_or_select_or_merge(
768776

769777
elif format == "zarr":
770778
assert folder is not None, "For format='zarr' folder must be provided"
771-
folder = Path(folder)
772-
if folder.suffix != ".zarr":
773-
folder = folder.parent / f"{folder.stem}.zarr"
779+
folder = clean_zarr_folder_name(folder)
774780
SortingAnalyzer.create_zarr(
775781
folder, sorting_provenance, recording, sparsity, self.return_scaled, self.rec_attributes
776782
)
@@ -829,6 +835,8 @@ def save_as(self, format="memory", folder=None) -> "SortingAnalyzer":
829835
format : "memory" | "binary_folder" | "zarr", default: "memory"
830836
The new backend format to use
831837
"""
838+
if format == "zarr":
839+
folder = clean_zarr_folder_name(folder)
832840
return self._save_or_select_or_merge(format=format, folder=folder)
833841

834842
def select_units(self, unit_ids, format="memory", folder=None) -> "SortingAnalyzer":
@@ -854,6 +862,8 @@ def select_units(self, unit_ids, format="memory", folder=None) -> "SortingAnalyz
854862
The newly create sorting_analyzer with the selected units
855863
"""
856864
# TODO check that unit_ids are in same order otherwise many extension do handle it properly!!!!
865+
if format == "zarr":
866+
folder = clean_zarr_folder_name(folder)
857867
return self._save_or_select_or_merge(format=format, folder=folder, unit_ids=unit_ids)
858868

859869
def remove_units(self, remove_unit_ids, format="memory", folder=None) -> "SortingAnalyzer":
@@ -880,6 +890,8 @@ def remove_units(self, remove_unit_ids, format="memory", folder=None) -> "Sortin
880890
"""
881891
# TODO check that unit_ids are in same order otherwise many extension do handle it properly!!!!
882892
unit_ids = self.unit_ids[~np.isin(self.unit_ids, remove_unit_ids)]
893+
if format == "zarr":
894+
folder = clean_zarr_folder_name(folder)
883895
return self._save_or_select_or_merge(format=format, folder=folder, unit_ids=unit_ids)
884896

885897
def merge_units(
@@ -938,6 +950,9 @@ def merge_units(
938950
The newly create `SortingAnalyzer` with the selected units
939951
"""
940952

953+
if format == "zarr":
954+
folder = clean_zarr_folder_name(folder)
955+
941956
assert merging_mode in ["soft", "hard"], "Merging mode should be either soft or hard"
942957

943958
if len(merge_unit_groups) == 0:
@@ -1016,6 +1031,9 @@ def has_temporary_recording(self) -> bool:
10161031
def is_sparse(self) -> bool:
10171032
return self.sparsity is not None
10181033

1034+
def is_filtered(self) -> bool:
1035+
return self.rec_attributes["is_filtered"]
1036+
10191037
def get_sorting_provenance(self):
10201038
"""
10211039
Get the original sorting if possible otherwise return None

src/spikeinterface/core/tests/test_recording_tools.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
get_channel_distances,
1818
get_noise_levels,
1919
order_channels_by_depth,
20+
do_recording_attributes_match,
21+
get_rec_attributes,
2022
)
2123

2224

@@ -300,6 +302,35 @@ def test_order_channels_by_depth():
300302
assert np.array_equal(order_2d[::-1], order_2d_fliped)
301303

302304

305+
def test_do_recording_attributes_match():
306+
recording = NoiseGeneratorRecording(
307+
num_channels=2, durations=[10.325, 3.5], sampling_frequency=30_000, strategy="tile_pregenerated"
308+
)
309+
rec_attributes = get_rec_attributes(recording)
310+
do_match, _ = do_recording_attributes_match(recording, rec_attributes)
311+
assert do_match
312+
313+
rec_attributes = get_rec_attributes(recording)
314+
rec_attributes["sampling_frequency"] = 1.0
315+
do_match, exc = do_recording_attributes_match(recording, rec_attributes)
316+
assert not do_match
317+
assert "sampling_frequency" in exc
318+
319+
# check dtype options
320+
rec_attributes = get_rec_attributes(recording)
321+
rec_attributes["dtype"] = "int16"
322+
do_match, exc = do_recording_attributes_match(recording, rec_attributes)
323+
assert not do_match
324+
assert "dtype" in exc
325+
do_match, exc = do_recording_attributes_match(recording, rec_attributes, check_dtype=False)
326+
assert do_match
327+
328+
# check missing dtype
329+
rec_attributes.pop("dtype")
330+
do_match, exc = do_recording_attributes_match(recording, rec_attributes)
331+
assert do_match
332+
333+
303334
if __name__ == "__main__":
304335
# Create a temporary folder using the standard library
305336
import tempfile

src/spikeinterface/core/tests/test_sortinganalyzer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def test_SortingAnalyzer_tmp_recording(dataset):
141141
recording_sliced = recording.channel_slice(recording.channel_ids[:-1])
142142

143143
# wrong channels
144-
with pytest.raises(AssertionError):
144+
with pytest.raises(ValueError):
145145
sorting_analyzer.set_temporary_recording(recording_sliced)
146146

147147

src/spikeinterface/exporters/to_phy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def export_to_phy(
167167
f.write(f"dtype = '{dtype_str}'\n")
168168
f.write(f"offset = 0\n")
169169
f.write(f"sample_rate = {fs}\n")
170-
f.write(f"hp_filtered = {sorting_analyzer.recording.is_filtered()}")
170+
f.write(f"hp_filtered = {sorting_analyzer.is_filtered()}")
171171

172172
# export spike_times/spike_templates/spike_clusters
173173
# here spike_labels is a remapping to unit_index

0 commit comments

Comments
 (0)