Skip to content

Commit b9c1e36

Browse files
committed
Merge with main
2 parents e14c2b2 + 8aeaf9b commit b9c1e36

36 files changed

Lines changed: 323 additions & 233 deletions

doc/how_to/benchmark_with_hybrid_recordings.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2531,9 +2531,8 @@ Although non of the sorters find all units perfectly, ``Kilosort2.5``,
25312531
``Kilosort4``, and ``SpyKING CIRCUS 2`` all find around 10-12 hybrid
25322532
units with accuracy greater than 80%. ``Kilosort4`` has a better overall
25332533
curve, being able to find almost all units with an accuracy above 50%.
2534-
``Kilosort2.5`` performs well when looking at precision (finding all
2535-
spikes in a hybrid unit), at the cost of lower recall (finding spikes
2536-
when it shouldn’t).
2534+
``Kilosort2.5`` performs well when looking at precision (not finding spikes
2535+
when it shouldn’t), but it has a lower recall (finding all spikes in the ground truth).
25372536

25382537
In this example, we showed how to:
25392538

examples/how_to/benchmark_with_hybrid_recordings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,8 @@
276276
# From the performance plots, we can see that there is no clear "winner", but `Kilosort3` definitely performs worse than the other options.
277277
#
278278
# Although non of the sorters find all units perfectly, `Kilosort2.5`, `Kilosort4`, and `SpyKING CIRCUS 2` all find around 10-12 hybrid units with accuracy greater than 80%.
279-
# `Kilosort4` has a better overall curve, being able to find almost all units with an accuracy above 50%. `Kilosort2.5` performs well when looking at precision (finding all spikes in a hybrid unit), at the cost of lower recall (finding spikes when it shouldn't).
279+
# `Kilosort4` has a better overall curve, being able to find almost all units with an accuracy above 50%. `Kilosort2.5` performs well when looking at precision (not finding spikes
280+
# when it shouldn’t), but it has a lower recall (finding all spikes in the ground truth).
280281
#
281282
#
282283
# In this example, we showed how to:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ dependencies = [
2323
"numpy>=1.20",
2424
"threadpoolctl>=3.0.0",
2525
"tqdm",
26-
"zarr>=2.16,<2.18",
26+
"zarr>=2.18,<3",
2727
"neo>=0.14.0",
2828
"probeinterface>=0.2.23",
2929
"packaging",

src/spikeinterface/benchmark/benchmark_base.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from spikeinterface.core import SortingAnalyzer
1313

14-
from spikeinterface import load_extractor, create_sorting_analyzer, load_sorting_analyzer
14+
from spikeinterface import load, create_sorting_analyzer, load_sorting_analyzer
1515
from spikeinterface.widgets import get_some_colors
1616

1717

@@ -150,13 +150,13 @@ def scan_folder(self):
150150
analyzer = load_sorting_analyzer(folder)
151151
self.analyzers[key] = analyzer
152152
# the sorting is in memory here we take the saved one because comparisons need to pickle it later
153-
sorting = load_extractor(analyzer.folder / "sorting")
153+
sorting = load(analyzer.folder / "sorting")
154154
self.datasets[key] = analyzer.recording, sorting
155155

156156
# for rec_file in (self.folder / "datasets" / "recordings").glob("*.pickle"):
157157
# key = rec_file.stem
158-
# rec = load_extractor(rec_file)
159-
# gt_sorting = load_extractor(self.folder / f"datasets" / "gt_sortings" / key)
158+
# rec = load(rec_file)
159+
# gt_sorting = load(self.folder / f"datasets" / "gt_sortings" / key)
160160
# self.datasets[key] = (rec, gt_sorting)
161161

162162
with open(self.folder / "cases.pickle", "rb") as f:
@@ -428,7 +428,7 @@ def load_folder(cls, folder):
428428
elif format == "sorting":
429429
from spikeinterface.core import load_extractor
430430

431-
result[k] = load_extractor(folder / k)
431+
result[k] = load(folder / k)
432432
elif format == "Motion":
433433
from spikeinterface.sortingcomponents.motion import Motion
434434

src/spikeinterface/comparison/multicomparisons.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import numpy as np
99

10-
from spikeinterface.core import load_extractor, BaseSorting, BaseSortingSegment
10+
from spikeinterface.core import load, BaseSorting, BaseSortingSegment
1111
from spikeinterface.core.core_tools import define_function_from_class
1212
from .basecomparison import BaseMultiComparison, MixinSpikeTrainComparison, MixinTemplateComparison
1313
from .paircomparisons import SymmetricSortingComparison, TemplateComparison
@@ -230,7 +230,7 @@ def load_from_folder(folder_path):
230230
with (folder_path / "sortings.json").open() as f:
231231
dict_sortings = json.load(f)
232232
name_list = list(dict_sortings.keys())
233-
sorting_list = [load_extractor(v, base_folder=folder_path) for v in dict_sortings.values()]
233+
sorting_list = [load(v, base_folder=folder_path) for v in dict_sortings.values()]
234234
mcmp = MultiSortingComparison(sorting_list=sorting_list, name_list=list(name_list), do_matching=False, **kwargs)
235235
filename = str(folder_path / "multicomparison.gpickle")
236236
with open(filename, "rb") as f:

src/spikeinterface/comparison/tests/test_hybrid.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22
import shutil
33
from pathlib import Path
4-
from spikeinterface.core import extract_waveforms, load_waveforms, load_extractor
4+
from spikeinterface.core import extract_waveforms, load_waveforms, load
55
from spikeinterface.core.testing import check_recordings_equal
66
from spikeinterface.comparison import (
77
create_hybrid_units_recording,
@@ -52,7 +52,7 @@ def test_hybrid_units_recording(setup_module):
5252
)
5353

5454
# Check dumpability
55-
saved_loaded = load_extractor(hybrid_units_recording.to_dict())
55+
saved_loaded = load(hybrid_units_recording.to_dict())
5656
check_recordings_equal(hybrid_units_recording, saved_loaded, return_scaled=False)
5757

5858
saved_1job = hybrid_units_recording.save(folder=cache_folder / "units_1job")
@@ -81,7 +81,7 @@ def test_hybrid_spikes_recording(setup_module):
8181
)
8282

8383
# Check dumpability
84-
saved_loaded = load_extractor(hybrid_spikes_recording.to_dict())
84+
saved_loaded = load(hybrid_spikes_recording.to_dict())
8585
check_recordings_equal(hybrid_spikes_recording, saved_loaded, return_scaled=False)
8686

8787
saved_1job = hybrid_spikes_recording.save(folder=cache_folder / "spikes_1job")

src/spikeinterface/core/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from .base import load_extractor # , load_extractor_from_dict, load_extractor_from_json, load_extractor_from_pickle
21
from .baserecording import BaseRecording, BaseRecordingSegment
32
from .basesorting import BaseSorting, BaseSortingSegment, SpikeVectorSortingSegment
43
from .baseevent import BaseEvent, BaseEventSegment
54
from .basesnippets import BaseSnippets, BaseSnippetsSegment
65
from .baserecordingsnippets import BaseRecordingSnippets
76

7+
from .loading import load, load_extractor
8+
89
# main extractor from dump and cache
910
from .binaryrecordingextractor import BinaryRecordingExtractor, read_binary
1011
from .npzsortingextractor import NpzSortingExtractor, read_npz_sorting

src/spikeinterface/core/base.py

Lines changed: 9 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from .globals import get_global_tmp_folder, is_set_global_tmp_folder
1818
from .core_tools import (
19-
check_json,
19+
is_path_remote,
2020
clean_zarr_folder_name,
2121
is_dict_extractor,
2222
SIJsonEncoder,
@@ -673,7 +673,7 @@ def dump_to_json(
673673
) -> None:
674674
"""
675675
Dump recording extractor to json file.
676-
The extractor can be re-loaded with load_extractor(json_file)
676+
The extractor can be re-loaded with load(json_file)
677677
678678
Parameters
679679
----------
@@ -715,7 +715,7 @@ def dump_to_pickle(
715715
):
716716
"""
717717
Dump recording extractor to a pickle file.
718-
The extractor can be re-loaded with load_extractor(pickle_file)
718+
The extractor can be re-loaded with load(pickle_file)
719719
720720
Parameters
721721
----------
@@ -752,7 +752,9 @@ def dump_to_pickle(
752752
file_path.write_bytes(pickle.dumps(dump_dict))
753753

754754
@staticmethod
755-
def load(file_path: Union[str, Path], base_folder: Optional[Union[Path, str, bool]] = None) -> "BaseExtractor":
755+
def load(
756+
file_or_folder_path: Union[str, Path], base_folder: Optional[Union[Path, str, bool]] = None
757+
) -> "BaseExtractor":
756758
"""
757759
Load extractor from file path (.json or .pkl)
758760
@@ -761,62 +763,10 @@ def load(file_path: Union[str, Path], base_folder: Optional[Union[Path, str, boo
761763
* save (...) a folder which contain data + json (or pickle) + metadata.
762764
763765
"""
766+
# use loading.py and keep backward compatibility
767+
from .loading import load
764768

765-
file_path = Path(file_path)
766-
if base_folder is True:
767-
base_folder = file_path.parent
768-
769-
if file_path.is_file():
770-
# standard case based on a file (json or pickle)
771-
if str(file_path).endswith(".json"):
772-
with open(file_path, "r") as f:
773-
d = json.load(f)
774-
elif str(file_path).endswith(".pkl") or str(file_path).endswith(".pickle"):
775-
with open(file_path, "rb") as f:
776-
d = pickle.load(f)
777-
else:
778-
raise ValueError(f"Impossible to load {file_path}")
779-
if "warning" in d:
780-
print("The extractor was not serializable to file")
781-
return None
782-
783-
extractor = BaseExtractor.from_dict(d, base_folder=base_folder)
784-
return extractor
785-
786-
elif file_path.is_dir():
787-
# case from a folder after a calling extractor.save(...)
788-
folder = file_path
789-
file = None
790-
791-
if folder.suffix == ".zarr":
792-
from .zarrextractors import read_zarr
793-
794-
extractor = read_zarr(folder)
795-
else:
796-
# the is spikeinterface<=0.94.0
797-
# a folder came with 'cached.json'
798-
for dump_ext in ("json", "pkl", "pickle"):
799-
f = folder / f"cached.{dump_ext}"
800-
if f.is_file():
801-
file = f
802-
803-
# spikeinterface>=0.95.0
804-
f = folder / f"si_folder.json"
805-
if f.is_file():
806-
file = f
807-
808-
if file is None:
809-
raise ValueError(f"This folder is not a cached folder {file_path}")
810-
extractor = BaseExtractor.load(file, base_folder=folder)
811-
812-
return extractor
813-
814-
else:
815-
error_msg = (
816-
f"{file_path} is not a file or a folder. It should point to either a json, pickle file or a "
817-
"folder that is the result of extractor.save(...)"
818-
)
819-
raise ValueError(error_msg)
769+
return load(file_or_folder_path, base_folder=base_folder)
820770

821771
def __reduce__(self):
822772
"""
@@ -1167,50 +1117,6 @@ def _check_same_version(class_string, version):
11671117
return "unknown"
11681118

11691119

1170-
def load_extractor(file_or_folder_or_dict, base_folder=None) -> BaseExtractor:
1171-
"""
1172-
Instantiate extractor from:
1173-
* a dict
1174-
* a json file
1175-
* a pickle file
1176-
* folder (after save)
1177-
* a zarr folder (after save)
1178-
1179-
Parameters
1180-
----------
1181-
file_or_folder_or_dict : dictionary or folder or file (json, pickle)
1182-
The file path, folder path, or dictionary to load the extractor from
1183-
base_folder : str | Path | bool (optional)
1184-
The base folder to make relative paths absolute.
1185-
If True and file_or_folder_or_dict is a file, the parent folder of the file is used.
1186-
1187-
Returns
1188-
-------
1189-
extractor: Recording or Sorting
1190-
The loaded extractor object
1191-
"""
1192-
if isinstance(file_or_folder_or_dict, dict):
1193-
assert not isinstance(base_folder, bool), "`base_folder` must be a string or Path when loading from dict"
1194-
return BaseExtractor.from_dict(file_or_folder_or_dict, base_folder=base_folder)
1195-
else:
1196-
return BaseExtractor.load(file_or_folder_or_dict, base_folder=base_folder)
1197-
1198-
1199-
def load_extractor_from_dict(d, base_folder=None) -> BaseExtractor:
1200-
warnings.warn("Use load_extractor(..) instead")
1201-
return BaseExtractor.from_dict(d, base_folder=base_folder)
1202-
1203-
1204-
def load_extractor_from_json(json_file, base_folder=None) -> "BaseExtractor":
1205-
warnings.warn("Use load_extractor(..) instead")
1206-
return BaseExtractor.load(json_file, base_folder=base_folder)
1207-
1208-
1209-
def load_extractor_from_pickle(pkl_file, base_folder=None) -> "BaseExtractor":
1210-
warnings.warn("Use load_extractor(..) instead")
1211-
return BaseExtractor.load(pkl_file, base_folder=base_folder)
1212-
1213-
12141120
class BaseSegment:
12151121
def __init__(self):
12161122
self._parent_extractor = None

src/spikeinterface/core/baserecording.py

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,9 @@ def get_duration(self, segment_index=None) -> float:
232232
float
233233
The duration in seconds
234234
"""
235-
segment_index = self._check_segment_index(segment_index)
236-
237-
if self.has_time_vector(segment_index):
238-
times = self.get_times(segment_index)
239-
segment_duration = times[-1] - times[0] + (1 / self.get_sampling_frequency())
240-
else:
241-
segment_num_samples = self.get_num_samples(segment_index=segment_index)
242-
segment_duration = segment_num_samples / self.get_sampling_frequency()
243-
235+
segment_duration = (
236+
self.get_end_time(segment_index) - self.get_start_time(segment_index) + (1 / self.get_sampling_frequency())
237+
)
244238
return segment_duration
245239

246240
def get_total_duration(self) -> float:
@@ -252,7 +246,7 @@ def get_total_duration(self) -> float:
252246
float
253247
The duration in seconds
254248
"""
255-
duration = sum([self.get_duration(idx) for idx in range(self.get_num_segments())])
249+
duration = sum([self.get_duration(segment_index) for segment_index in range(self.get_num_segments())])
256250
return duration
257251

258252
def get_memory_size(self, segment_index=None) -> int:
@@ -445,6 +439,40 @@ def get_times(self, segment_index=None) -> np.ndarray:
445439
times = rs.get_times()
446440
return times
447441

442+
def get_start_time(self, segment_index=None) -> float:
443+
"""Get the start time of the recording segment.
444+
445+
Parameters
446+
----------
447+
segment_index : int or None, default: None
448+
The segment index (required for multi-segment)
449+
450+
Returns
451+
-------
452+
float
453+
The start time in seconds
454+
"""
455+
segment_index = self._check_segment_index(segment_index)
456+
rs = self._recording_segments[segment_index]
457+
return rs.get_start_time()
458+
459+
def get_end_time(self, segment_index=None) -> float:
460+
"""Get the stop time of the recording segment.
461+
462+
Parameters
463+
----------
464+
segment_index : int or None, default: None
465+
The segment index (required for multi-segment)
466+
467+
Returns
468+
-------
469+
float
470+
The stop time in seconds
471+
"""
472+
segment_index = self._check_segment_index(segment_index)
473+
rs = self._recording_segments[segment_index]
474+
return rs.get_end_time()
475+
448476
def has_time_vector(self, segment_index=None):
449477
"""Check if the segment of the recording has a time vector.
450478
@@ -903,6 +931,21 @@ def get_times(self) -> np.ndarray:
903931
time_vector += self.t_start
904932
return time_vector
905933

934+
def get_start_time(self) -> float:
935+
if self.time_vector is not None:
936+
return self.time_vector[0]
937+
else:
938+
return self.t_start if self.t_start is not None else 0.0
939+
940+
def get_end_time(self) -> float:
941+
if self.time_vector is not None:
942+
return self.time_vector[-1]
943+
else:
944+
t_stop = (self.get_num_samples() - 1) / self.sampling_frequency
945+
if self.t_start is not None:
946+
t_stop += self.t_start
947+
return t_stop
948+
906949
def get_times_kwargs(self) -> dict:
907950
"""
908951
Retrieves the timing attributes characterizing a RecordingSegment

0 commit comments

Comments
 (0)