Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6a4c490
mode the Base.save to Sorting.save() and Recording.save()
samuelgarcia Jul 9, 2026
2e386b5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 9, 2026
e5c1dec
Merge with main and debug
samuelgarcia Sep 8, 2026
14c780a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 8, 2026
b4d184c
Fix some rec.save() without folder
samuelgarcia Sep 8, 2026
860643e
Merge branch 'refactor_base_save_dump' of github.com:samuelgarcia/spi…
samuelgarcia Sep 8, 2026
b6636ad
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 8, 2026
c78c84a
Create an explicit annoations.json file for BinaryFolderRecording and…
samuelgarcia Sep 8, 2026
4d1253c
Merge branch 'refactor_base_save_dump' of github.com:samuelgarcia/spi…
samuelgarcia Sep 8, 2026
653bfff
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 8, 2026
8ed0644
fix: save() functions in tests
alejoe91 Sep 8, 2026
a890482
fix: tests and add some TODOs
alejoe91 Sep 9, 2026
67a8d2d
refactor: cleanup save functions and add detailed docs
alejoe91 Sep 9, 2026
cbb8156
fix: zarr loading and rename numpy_folder -> binary
alejoe91 Sep 9, 2026
4dc62cb
fix: remove folder_metadata, add time_kwargs to dump_dict if time_inf…
alejoe91 Sep 9, 2026
67e79a2
Merge branch 'main' into refactor_base_save_dump
alejoe91 Sep 9, 2026
280ba86
Merge branch 'main' into refactor_base_save_dump
alejoe91 Sep 10, 2026
b4287c6
Merge branch 'main' into refactor_base_save_dump
alejoe91 Sep 15, 2026
4909ec1
revert numpy_folder change
alejoe91 Sep 16, 2026
a3b14ef
Merge branch 'main' of github.com:SpikeInterface/spikeinterface into …
alejoe91 Sep 16, 2026
51c6b51
fix: make TimeSeries a class to instantiate self._time_info_modified
alejoe91 Sep 16, 2026
a2301ec
refactor: rename save property functions and fix docstrings
alejoe91 Sep 16, 2026
443f80a
feat: add main_ids to dump_dict
alejoe91 Sep 16, 2026
f0b7637
fix: benchmark base
alejoe91 Sep 16, 2026
874542e
fix: resample test
alejoe91 Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 39 additions & 11 deletions .github/scripts/serialization/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@
"json": ".json",
"pickle": ".pkl",
"binary": "_binary",
"binary_parallel": "_binary_parallel",
"numpy_folder": "_numpy_folder",
"zarr": ".zarr",
"zarr_parallel": "_parallel.zarr",
}

DEFAULT_DURATION = 2.0 # seconds, for recordings and sortings

# --- json (recipe) entries: moved class + a second class -------------------------

Expand All @@ -46,7 +49,12 @@ def _build_noise_generator_recording():
except ImportError:
from spikeinterface.core.generate import NoiseGeneratorRecording

return NoiseGeneratorRecording(num_channels=4, sampling_frequency=30000.0, durations=[1.0, 1.5], seed=0)
return NoiseGeneratorRecording(
num_channels=4,
sampling_frequency=30000.0,
durations=[DEFAULT_DURATION, DEFAULT_DURATION + 0.5],
seed=0,
)


def _check_noise_generator_recording(rec):
Expand All @@ -59,7 +67,7 @@ def _check_noise_generator_recording(rec):
def _build_mock_recording():
from spikeinterface.core import generate_recording

return generate_recording(num_channels=4, durations=[1.0], sampling_frequency=30000.0, seed=0)
return generate_recording(num_channels=4, durations=[DEFAULT_DURATION], sampling_frequency=30000.0, seed=0)


def _check_mock_recording(rec):
Expand All @@ -77,7 +85,7 @@ def _build_recording_with_properties():
import numpy as np
from spikeinterface.core import generate_recording

rec = generate_recording(num_channels=4, durations=[1.0], sampling_frequency=30000.0, seed=0)
rec = generate_recording(num_channels=4, durations=[DEFAULT_DURATION], sampling_frequency=30000.0, seed=0)
rec.set_property("quality", np.array(["good", "good", "bad", "good"]))
rec.annotate(experimenter="test")
return rec
Expand All @@ -94,7 +102,7 @@ def _build_recording_with_probe():
from probeinterface import generate_linear_probe
from spikeinterface.core import generate_recording

rec = generate_recording(num_channels=8, durations=[1.0], sampling_frequency=30000.0, seed=0)
rec = generate_recording(num_channels=8, durations=[DEFAULT_DURATION], sampling_frequency=30000.0, seed=0)
probe = generate_linear_probe(num_elec=8)
probe.set_device_channel_indices(np.arange(8))
if parse(si_version) <= parse("0.105.0"):
Expand All @@ -104,6 +112,20 @@ def _build_recording_with_probe():
return rec_with_probe


def _build_recording_with_timestamps():
rec = _build_mock_recording()
times = rec.get_times(segment_index=0) + 100
rec.set_times(times, segment_index=0)
return rec


def _check_recording_with_timestamps(rec):
import numpy as np
expected_times = np.arange(int(DEFAULT_DURATION * 30000)) / 30000.0 + 100
times = rec.get_times(segment_index=0)
assert np.allclose(times, expected_times)


def _check_recording_with_probe(rec):
import numpy as np

Expand All @@ -121,7 +143,7 @@ def _build_recording_with_interleaved_probes():
from probeinterface import ProbeGroup, generate_linear_probe
from spikeinterface.core import generate_recording

rec = generate_recording(num_channels=8, durations=[1.0], sampling_frequency=30000.0, seed=0)
rec = generate_recording(num_channels=8, durations=[DEFAULT_DURATION], sampling_frequency=30000.0, seed=0)
probe0 = generate_linear_probe(num_elec=4)
probe1 = generate_linear_probe(num_elec=4)
probe1.move([100.0, 0.0])
Expand Down Expand Up @@ -164,7 +186,7 @@ def _build_preprocessed_chain():
from spikeinterface.core import generate_recording
from spikeinterface.preprocessing import common_reference, scale

rec = generate_recording(num_channels=4, durations=[1.0], sampling_frequency=30000.0, seed=0)
rec = generate_recording(num_channels=4, durations=[DEFAULT_DURATION], sampling_frequency=30000.0, seed=0)
# Two nested scipy-free preprocessing wrappers (scale then common_reference): this
# exercises recursive parent reload without pulling scipy into the environments.
return common_reference(scale(rec, gain=2.0))
Expand All @@ -181,7 +203,7 @@ def _check_preprocessed_chain(rec):
def _build_sorting():
from spikeinterface.core import generate_sorting

return generate_sorting(num_units=5, sampling_frequency=30000.0, durations=[1.0])
return generate_sorting(num_units=5, sampling_frequency=30000.0, durations=[DEFAULT_DURATION])


def _check_sorting(sorting):
Expand All @@ -195,7 +217,7 @@ def _build_sorting_with_properties():
import numpy as np
from spikeinterface.core import generate_sorting

sorting = generate_sorting(num_units=4, sampling_frequency=30000.0, durations=[1.0])
sorting = generate_sorting(num_units=4, sampling_frequency=30000.0, durations=[DEFAULT_DURATION])
sorting.set_property("quality", np.array(["good", "good", "bad", "good"]))
sorting.annotate(experimenter="test")
return sorting
Expand Down Expand Up @@ -224,19 +246,25 @@ def _check_sorting_with_properties(sorting):
"id": "recording_with_properties",
"build": _build_recording_with_properties,
"check": _check_recording_with_properties,
"formats": ["binary", "zarr"],
"formats": ["binary", "zarr", "binary_parallel", "zarr_parallel"],
},
{
"id": "recording_with_timestamps",
"build": _build_recording_with_timestamps,
"check": _check_recording_with_timestamps,
"formats": ["binary", "zarr", "binary_parallel", "zarr_parallel"],
},
{
"id": "recording_with_probe",
"build": _build_recording_with_probe,
"check": _check_recording_with_probe,
"formats": ["binary", "zarr"],
"formats": ["binary", "zarr", "binary_parallel", "zarr_parallel"],
},
{
"id": "recording_with_interleaved_probes",
"build": _build_recording_with_interleaved_probes,
"check": _check_recording_with_interleaved_probes,
"formats": ["binary", "zarr"],
"formats": ["binary", "zarr", "binary_parallel", "zarr_parallel"],
},
{
"id": "preprocessed_chain",
Expand Down
4 changes: 4 additions & 0 deletions .github/scripts/serialization/serialize_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@
obj.dump_to_pickle(dest)
elif fmt == "binary":
obj.save(folder=dest, format="binary", overwrite=True)
elif fmt == "binary_parallel":
obj.save(folder=dest, format="binary", overwrite=True, n_jobs=2)
elif fmt == "numpy_folder":
obj.save(folder=dest, format="numpy_folder", overwrite=True)
elif fmt == "zarr":
obj.save(folder=dest, format="zarr", overwrite=True)
elif fmt == "zarr_parallel":
obj.save(folder=dest, format="zarr", overwrite=True, n_jobs=2)
print(f" wrote {dest.name} ({fmt})")

print(f"Fixtures written to: {out_dir.resolve()}")
2 changes: 1 addition & 1 deletion .github/scripts/test_kilosort4_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ def test_use_binary_file(self, tmp_path):
from the recording.
"""
recording = self._get_ground_truth_recording()
recording_bin = recording.save()
recording_bin = recording.save(folder=tmp_path / "recording_for_ks4")

# run with SI wrapper
sorting_ks4 = si.run_sorter(
Expand Down
22 changes: 7 additions & 15 deletions .github/workflows/cross_version_serialization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ name: Cross-version serialization
#
# Delivery model: Live generation. Fixtures are regenerated from a real old install each
# run in an isolated uv environment (nothing committed). The version list is computed
# from PyPI (latest patch of each minor at or above MIN_VERSION_TO_TEST): on a pull
# request only the latest released minor is tested; the full matrix runs weekly and on
# manual dispatch.
# from PyPI (latest patch of each minor at or above MIN_VERSION_TO_TEST), and the full
# matrix is tested on every pull request and on manual dispatch.

on:
pull_request:
Expand All @@ -20,11 +19,9 @@ on:
# watches core only; a break introduced in generation/ or preprocessing/ would not
# trigger it.
paths:
- "src/spikeinterface/core/**"
- ".github/scripts/serialization/**"
- ".github/workflows/cross_version_serialization.yml"
schedule:
- cron: "0 4 * * 0" # weekly, Sunday 04:00 UTC
- 'src/spikeinterface/core/**'
- '.github/scripts/serialization/**'
- '.github/workflows/cross_version_serialization.yml'
workflow_dispatch:

concurrency:
Expand All @@ -33,8 +30,8 @@ concurrency:

jobs:
# Compute which released versions the matrix below tests loading from, exposed as the
# job output `list`. Latest patch of each minor at or above the floor on schedule /
# dispatch; only the latest minor on a pull request (keeps per-PR runs cheap).
# job output `list`: the latest patch of each minor at or above the floor, tested on
# every trigger.
versions:
name: Select versions
runs-on: ubuntu-latest
Expand All @@ -46,7 +43,6 @@ jobs:
python-version: "3.10"
- id: set
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
# Support floor: oldest minor to test. Below this, installs fail on the CI
# Python (numcodecs dependency rot in 0.100/0.101). Raise when 0.102 rots.
MIN_VERSION_TO_TEST: "0.102" # We will change this when making breaking changes (hopefully not too often)
Expand All @@ -67,10 +63,6 @@ jobs:
candidates = [v for v in available if not v.is_prerelease and (v.major, v.minor) >= (floor.major, floor.minor)]
minor_releases = [max(group) for _, group in groupby(candidates, key=lambda v: (v.major, v.minor))]

# on a pull request, test only the latest minor
if os.environ.get('GITHUB_EVENT_NAME') == 'pull_request':
minor_releases = minor_releases[-1:]

print(json.dumps([str(v) for v in minor_releases]))
")

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

spikeinterface/widgets/tests/mearec_test/*

# for sam
dev_*.ipynb
dev_*.py

# Vscode
.vscode/*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,20 @@ def setup_module(tmp_path_factory):
return multicomparison_folder


def make_sorting(times1, labels1, times2, labels2, times3, labels3):
def make_sorting(times1, labels1, times2, labels2, times3, labels3, sorting_folder):
sampling_frequency = 30000.0
sorting1 = NumpySorting.from_samples_and_labels([times1], [labels1], sampling_frequency)
sorting2 = NumpySorting.from_samples_and_labels([times2], [labels2], sampling_frequency)
sorting3 = NumpySorting.from_samples_and_labels([times3], [labels3], sampling_frequency)
sorting1 = sorting1.save()
sorting2 = sorting2.save()
sorting3 = sorting3.save()
sorting1 = sorting1.save(folder=sorting_folder / "sorting1")
sorting2 = sorting2.save(folder=sorting_folder / "sorting2")
sorting3 = sorting3.save(folder=sorting_folder / "sorting3")
return sorting1, sorting2, sorting3


def test_compare_multiple_sorters(setup_module):
multicomparison_folder = setup_module
sorting_folder = multicomparison_folder.parent
# simple match
sorting1, sorting2, sorting3 = make_sorting(
[100, 200, 300, 400, 500, 600, 700, 800, 900],
Expand All @@ -43,6 +44,7 @@ def test_compare_multiple_sorters(setup_module):
[0, 1, 2, 0, 1, 2, 0, 1, 2, 3, 3, 4, 4],
[101, 201, 301, 400, 500, 600, 700, 800, 900, 1000, 1100, 2000, 3000, 3100, 3200, 3300],
[0, 1, 2, 0, 1, 2, 0, 1, 2, 3, 3, 4, 4, 5, 5, 5],
sorting_folder,
)
msc = compare_multiple_sorters([sorting1, sorting2, sorting3], verbose=True)
msc_shuffle = compare_multiple_sorters([sorting3, sorting1, sorting2])
Expand Down
Loading
Loading