Skip to content

Commit eb03db8

Browse files
authored
Merge branch 'main' into verbose_to_non_parallel_case
2 parents 33d3592 + d18cec2 commit eb03db8

49 files changed

Lines changed: 953 additions & 390 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ full = [
9595
"scikit-learn",
9696
"networkx",
9797
"distinctipy",
98-
"matplotlib<3.9", # See https://github.com/SpikeInterface/spikeinterface/issues/2863
98+
"matplotlib>=3.6", # matplotlib.colormaps
9999
"cuda-python; platform_system != 'Darwin'",
100100
"numba",
101101
]
@@ -159,8 +159,8 @@ test = [
159159
]
160160

161161
docs = [
162-
"Sphinx==5.1.1",
163-
"sphinx_rtd_theme==1.0.0",
162+
"Sphinx",
163+
"sphinx_rtd_theme",
164164
"sphinx-gallery",
165165
"sphinx-design",
166166
"numpydoc",
@@ -173,6 +173,7 @@ docs = [
173173
"hdbscan>=0.8.33", # For sorters spykingcircus2 + tridesclous
174174
"numba", # For many postprocessing functions
175175
"xarray", # For use of SortingAnalyzer zarr format
176+
"networkx",
176177
# for release we need pypi, so this needs to be commented
177178
"probeinterface @ git+https://github.com/SpikeInterface/probeinterface.git", # We always build from the latest version
178179
"neo @ git+https://github.com/NeuralEnsemble/python-neo.git", # We always build from the latest version

src/spikeinterface/core/analyzer_extension_core.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -488,9 +488,9 @@ def get_templates(self, unit_ids=None, operator="average", percentile=None, save
488488
self.params["operators"] += [(operator, percentile)]
489489
templates_array = self.data[key]
490490

491-
if save:
492-
if not self.sorting_analyzer.is_read_only():
493-
self.save()
491+
if save:
492+
if not self.sorting_analyzer.is_read_only():
493+
self.save()
494494

495495
if unit_ids is not None:
496496
unit_indices = self.sorting_analyzer.sorting.ids_to_indices(unit_ids)

src/spikeinterface/core/baserecording.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ def time_to_sample_index(self, time_s, segment_index=None):
491491
rs = self._recording_segments[segment_index]
492492
return rs.time_to_sample_index(time_s)
493493

494-
def _save(self, format="binary", **save_kwargs):
494+
def _save(self, format="binary", verbose: bool = False, **save_kwargs):
495495
# handle t_starts
496496
t_starts = []
497497
has_time_vectors = []
@@ -510,7 +510,7 @@ def _save(self, format="binary", **save_kwargs):
510510
file_paths = [folder / f"traces_cached_seg{i}.raw" for i in range(self.get_num_segments())]
511511
dtype = kwargs.get("dtype", None) or self.get_dtype()
512512

513-
write_binary_recording(self, file_paths=file_paths, dtype=dtype, **job_kwargs)
513+
write_binary_recording(self, file_paths=file_paths, dtype=dtype, verbose=verbose, **job_kwargs)
514514

515515
from .binaryrecordingextractor import BinaryRecordingExtractor
516516

@@ -540,14 +540,18 @@ def _save(self, format="binary", **save_kwargs):
540540

541541
cached = SharedMemoryRecording.from_recording(self, **job_kwargs)
542542
else:
543+
from spikeinterface.core import NumpyRecording
544+
543545
cached = NumpyRecording.from_recording(self, **job_kwargs)
544546

545547
elif format == "zarr":
546548
from .zarrextractors import ZarrRecordingExtractor
547549

548550
zarr_path = kwargs.pop("zarr_path")
549551
storage_options = kwargs.pop("storage_options")
550-
ZarrRecordingExtractor.write_recording(self, zarr_path, storage_options, **kwargs, **job_kwargs)
552+
ZarrRecordingExtractor.write_recording(
553+
self, zarr_path, storage_options, verbose=verbose, **kwargs, **job_kwargs
554+
)
551555
cached = ZarrRecordingExtractor(zarr_path, storage_options)
552556

553557
elif format == "nwb":

src/spikeinterface/core/job_tools.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
"chunk_duration",
4747
"progress_bar",
4848
"mp_context",
49-
"verbose",
5049
"max_threads_per_process",
5150
)
5251

@@ -131,6 +130,8 @@ def split_job_kwargs(mixed_kwargs):
131130
def divide_segment_into_chunks(num_frames, chunk_size):
132131
if chunk_size is None:
133132
chunks = [(0, num_frames)]
133+
elif chunk_size > num_frames:
134+
chunks = [(0, num_frames)]
134135
else:
135136
n = num_frames // chunk_size
136137

@@ -245,12 +246,12 @@ def ensure_chunk_size(
245246
else:
246247
raise ValueError("chunk_duration must be str or float")
247248
else:
249+
# Edge case to define single chunk per segment for n_jobs=1.
250+
# All chunking parameters equal None mean single chunk per segment
248251
if n_jobs == 1:
249-
# not chunk computing
250-
# TODO Discuss, Sam, is this something that we want to do?
251-
# Even in single process mode, we should chunk the data to avoid loading the whole thing into memory I feel
252-
# Am I wrong?
253-
chunk_size = None
252+
num_segments = recording.get_num_segments()
253+
samples_in_larger_segment = max([recording.get_num_samples(segment) for segment in range(num_segments)])
254+
chunk_size = samples_in_larger_segment
254255
else:
255256
raise ValueError("For n_jobs >1 you must specify total_memory or chunk_size or chunk_memory")
256257

src/spikeinterface/core/recording_tools.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def write_binary_recording(
7373
add_file_extension: bool = True,
7474
byte_offset: int = 0,
7575
auto_cast_uint: bool = True,
76+
verbose: bool = False,
7677
**job_kwargs,
7778
):
7879
"""
@@ -98,6 +99,8 @@ def write_binary_recording(
9899
auto_cast_uint: bool, default: True
99100
If True, unsigned integers are automatically cast to int if the specified dtype is signed
100101
.. deprecated:: 0.103, use the `unsigned_to_signed` function instead.
102+
verbose: bool
103+
This is the verbosity of the ChunkRecordingExecutor
101104
{}
102105
"""
103106
job_kwargs = fix_job_kwargs(job_kwargs)
@@ -138,7 +141,7 @@ def write_binary_recording(
138141
init_func = _init_binary_worker
139142
init_args = (recording, file_path_dict, dtype, byte_offset, cast_unsigned)
140143
executor = ChunkRecordingExecutor(
141-
recording, func, init_func, init_args, job_name="write_binary_recording", **job_kwargs
144+
recording, func, init_func, init_args, job_name="write_binary_recording", verbose=verbose, **job_kwargs
142145
)
143146
executor.run()
144147

@@ -348,9 +351,6 @@ def write_memory_recording(recording, dtype=None, verbose=False, auto_cast_uint=
348351
else:
349352
init_args = (recording, arrays, None, None, dtype, cast_unsigned)
350353

351-
if "verbose" in job_kwargs:
352-
del job_kwargs["verbose"]
353-
354354
executor = ChunkRecordingExecutor(
355355
recording, func, init_func, init_args, verbose=verbose, job_name="write_memory_recording", **job_kwargs
356356
)

src/spikeinterface/core/sortinganalyzer.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from itertools import chain
66
import os
77
import json
8+
import math
89
import pickle
910
import weakref
1011
import shutil
@@ -237,7 +238,21 @@ def create(
237238
return_scaled=True,
238239
):
239240
# some checks
240-
assert sorting.sampling_frequency == recording.sampling_frequency
241+
if sorting.sampling_frequency != recording.sampling_frequency:
242+
if math.isclose(sorting.sampling_frequency, recording.sampling_frequency, abs_tol=1e-2, rel_tol=1e-5):
243+
warnings.warn(
244+
"Sorting and Recording have a small difference in sampling frequency. "
245+
"This could be due to rounding of floats. Using the sampling frequency from the Recording."
246+
)
247+
# we make a copy here to change the smapling frequency
248+
sorting = NumpySorting.from_sorting(sorting, with_metadata=True, copy_spike_vector=True)
249+
sorting._sampling_frequency = recording.sampling_frequency
250+
else:
251+
raise ValueError(
252+
f"Sorting and Recording sampling frequencies are too different: "
253+
f"recording: {recording.sampling_frequency} - sorting: {sorting.sampling_frequency}. "
254+
"Ensure that you are associating the correct Recording and Sorting when creating a SortingAnalyzer."
255+
)
241256
# check that multiple probes are non-overlapping
242257
all_probes = recording.get_probegroup().probes
243258
check_probe_do_not_overlap(all_probes)
@@ -570,9 +585,10 @@ def load_from_zarr(cls, folder, recording=None):
570585
rec_attributes["probegroup"] = None
571586

572587
# sparsity
573-
if "sparsity_mask" in zarr_root.attrs:
574-
# sparsity = zarr_root.attrs["sparsity"]
575-
sparsity = ChannelSparsity(zarr_root["sparsity_mask"], cls.unit_ids, rec_attributes["channel_ids"])
588+
if "sparsity_mask" in zarr_root:
589+
sparsity = ChannelSparsity(
590+
np.array(zarr_root["sparsity_mask"]), sorting.unit_ids, rec_attributes["channel_ids"]
591+
)
576592
else:
577593
sparsity = None
578594

@@ -1581,10 +1597,6 @@ def load_data(self):
15811597
self.data[ext_data_name] = ext_data
15821598

15831599
elif self.format == "zarr":
1584-
# Alessio
1585-
# TODO: we need decide if we make a copy to memory or keep the lazy loading. For binary_folder it used to be lazy with memmap
1586-
# but this make the garbage complicated when a data is hold by a plot but the o SortingAnalyzer is delete
1587-
# lets talk
15881600
extension_group = self._get_zarr_extension_group(mode="r")
15891601
for ext_data_name in extension_group.keys():
15901602
ext_data_ = extension_group[ext_data_name]
@@ -1600,7 +1612,8 @@ def load_data(self):
16001612
elif "object" in ext_data_.attrs:
16011613
ext_data = ext_data_[0]
16021614
else:
1603-
ext_data = ext_data_
1615+
# this load in memmory
1616+
ext_data = np.array(ext_data_)
16041617
self.data[ext_data_name] = ext_data
16051618

16061619
def copy(self, new_sorting_analyzer, unit_ids=None):

src/spikeinterface/core/sparsity.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ def __repr__(self):
118118
txt = f"ChannelSparsity - units: {self.num_units} - channels: {self.num_channels} - density, P(x=1): {density:0.2f}"
119119
return txt
120120

121+
def __eq__(self, other):
122+
return (
123+
isinstance(other, ChannelSparsity)
124+
and np.array_equal(self.channel_ids, other.channel_ids)
125+
and np.array_equal(self.unit_ids, other.unit_ids)
126+
and np.array_equal(self.mask, other.mask)
127+
)
128+
121129
@property
122130
def unit_id_to_channel_ids(self):
123131
if self._unit_id_to_channel_ids is None:

src/spikeinterface/core/tests/test_job_tools.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,9 @@ def test_ensure_n_jobs():
4242

4343

4444
def test_ensure_chunk_size():
45-
recording = generate_recording(num_channels=2)
45+
recording = generate_recording(num_channels=2, durations=[5.0, 2.5]) # This is the default value for two semgents
4646
dtype = recording.get_dtype()
4747
assert dtype == "float32"
48-
# make serializable
49-
recording = recording.save()
5048

5149
chunk_size = ensure_chunk_size(recording, total_memory="512M", chunk_size=None, chunk_memory=None, n_jobs=2)
5250
assert chunk_size == 32000000
@@ -69,6 +67,15 @@ def test_ensure_chunk_size():
6967
chunk_size = ensure_chunk_size(recording, chunk_duration="500ms")
7068
assert chunk_size == 15000
7169

70+
# Test edge case to define single chunk for n_jobs=1
71+
chunk_size = ensure_chunk_size(recording, n_jobs=1, chunk_size=None)
72+
chunks = divide_recording_into_chunks(recording, chunk_size)
73+
assert len(chunks) == recording.get_num_segments()
74+
for chunk in chunks:
75+
segment_index, start_frame, end_frame = chunk
76+
assert start_frame == 0
77+
assert end_frame == recording.get_num_frames(segment_index=segment_index)
78+
7279

7380
def func(segment_index, start_frame, end_frame, worker_ctx):
7481
import os

src/spikeinterface/core/tests/test_recording_tools.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ def test_write_binary_recording(tmp_path):
3737
file_paths = [tmp_path / "binary01.raw"]
3838

3939
# Write binary recording
40-
job_kwargs = dict(verbose=False, n_jobs=1)
41-
write_binary_recording(recording, file_paths=file_paths, dtype=dtype, **job_kwargs)
40+
job_kwargs = dict(n_jobs=1)
41+
write_binary_recording(recording, file_paths=file_paths, dtype=dtype, verbose=False, **job_kwargs)
4242

4343
# Check if written data matches original data
4444
recorder_binary = BinaryRecordingExtractor(
@@ -64,9 +64,11 @@ def test_write_binary_recording_offset(tmp_path):
6464
file_paths = [tmp_path / "binary01.raw"]
6565

6666
# Write binary recording
67-
job_kwargs = dict(verbose=False, n_jobs=1)
67+
job_kwargs = dict(n_jobs=1)
6868
byte_offset = 125
69-
write_binary_recording(recording, file_paths=file_paths, dtype=dtype, byte_offset=byte_offset, **job_kwargs)
69+
write_binary_recording(
70+
recording, file_paths=file_paths, dtype=dtype, byte_offset=byte_offset, verbose=False, **job_kwargs
71+
)
7072

7173
# Check if written data matches original data
7274
recorder_binary = BinaryRecordingExtractor(
@@ -97,8 +99,8 @@ def test_write_binary_recording_parallel(tmp_path):
9799
file_paths = [tmp_path / "binary01.raw", tmp_path / "binary02.raw"]
98100

99101
# Write binary recording
100-
job_kwargs = dict(verbose=False, n_jobs=2, chunk_memory="100k", mp_context="spawn")
101-
write_binary_recording(recording, file_paths=file_paths, dtype=dtype, **job_kwargs)
102+
job_kwargs = dict(n_jobs=2, chunk_memory="100k", mp_context="spawn")
103+
write_binary_recording(recording, file_paths=file_paths, dtype=dtype, verbose=False, **job_kwargs)
102104

103105
# Check if written data matches original data
104106
recorder_binary = BinaryRecordingExtractor(
@@ -127,8 +129,8 @@ def test_write_binary_recording_multiple_segment(tmp_path):
127129
file_paths = [tmp_path / "binary01.raw", tmp_path / "binary02.raw"]
128130

129131
# Write binary recording
130-
job_kwargs = dict(verbose=False, n_jobs=2, chunk_memory="100k", mp_context="spawn")
131-
write_binary_recording(recording, file_paths=file_paths, dtype=dtype, **job_kwargs)
132+
job_kwargs = dict(n_jobs=2, chunk_memory="100k", mp_context="spawn")
133+
write_binary_recording(recording, file_paths=file_paths, dtype=dtype, verbose=False, **job_kwargs)
132134

133135
# Check if written data matches original data
134136
recorder_binary = BinaryRecordingExtractor(

src/spikeinterface/core/tests/test_sortinganalyzer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,15 @@ def _check_sorting_analyzers(sorting_analyzer, original_sorting, cache_folder):
155155

156156
data = sorting_analyzer2.get_extension("dummy").data
157157
assert "result_one" in data
158+
assert isinstance(data["result_one"], str)
159+
assert isinstance(data["result_two"], np.ndarray)
158160
assert data["result_two"].size == original_sorting.to_spike_vector().size
161+
assert np.array_equal(data["result_two"], sorting_analyzer.get_extension("dummy").data["result_two"])
159162

160163
assert sorting_analyzer2.return_scaled == sorting_analyzer.return_scaled
161164

165+
assert sorting_analyzer2.sparsity == sorting_analyzer.sparsity
166+
162167
# select unit_ids to several format
163168
for format in ("memory", "binary_folder", "zarr"):
164169
if format != "memory":

0 commit comments

Comments
 (0)