Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
adf8c9e
wip: support zarr v3
alejoe91 Dec 11, 2025
4dbb7b3
wip
alejoe91 Dec 12, 2025
ff3612e
tests: add test for backward compatibility v2v3
alejoe91 Mar 23, 2026
4bc45da
fix: zarr.Group
alejoe91 Mar 23, 2026
e9a567f
wip: fix v3
alejoe91 Mar 23, 2026
39a7588
Fix v2/v3 test, add zarr_class_info, pin to dev probeinterface for te…
alejoe91 Mar 25, 2026
348ff80
fix conflicts
alejoe91 Mar 25, 2026
7be375b
Python>=3.11 and disable deepinterpolation action
alejoe91 Mar 25, 2026
38987af
Add sharding option
alejoe91 Mar 25, 2026
f173786
wrong imports
alejoe91 Mar 25, 2026
42bb521
fix: channel_ids dtype
alejoe91 Mar 26, 2026
94b6ea2
fix conflicts and install dev hdmf-zarr
alejoe91 Mar 26, 2026
0de855b
Fix NWB-zarr tests
alejoe91 Mar 26, 2026
ab669a6
Fix conflicts
alejoe91 Mar 26, 2026
79a104d
update s3fs
alejoe91 Mar 31, 2026
008b5e2
Fix nwb reading
alejoe91 Mar 31, 2026
87841e4
Fix streaming extractors tests
alejoe91 Mar 31, 2026
499de27
Merge branch 'main' into zarrv3
alejoe91 Mar 31, 2026
8446c40
solve conflicts
alejoe91 Jun 9, 2026
7c1f30e
Merge branch 'zarrv3' of github.com:alejoe91/spikeinterface into zarrv3
alejoe91 Jun 9, 2026
53d9315
fix: imports
alejoe91 Jun 9, 2026
14f272e
fix: update test dependencies
alejoe91 Jun 9, 2026
bef4ec2
fix: merge with main and solve conflicts
alejoe91 Jun 24, 2026
8e686fb
fix: conflcits 2
alejoe91 Jun 24, 2026
4e35afb
fix: pipeline from analyzer
alejoe91 Jun 24, 2026
3aee605
fix: zarr backward compatibility tests
alejoe91 Jun 24, 2026
b62c0bc
docs: fix build
alejoe91 Jun 24, 2026
0a435f1
fix: conflicts
alejoe91 Jul 2, 2026
c0aa439
fix: conflicts
alejoe91 Jul 15, 2026
17d45f8
Merge branch 'zarrv3' of https://github.com/alejoe91/spikeinterface i…
alejoe91 Jul 15, 2026
d5f2e96
Merge branch 'main' into zarrv3
alejoe91 Jul 15, 2026
054ecf9
fix: sorting analzyer zarr tests
alejoe91 Jul 15, 2026
0615323
Merge branch 'zarrv3' of https://github.com/alejoe91/spikeinterface i…
alejoe91 Jul 15, 2026
9bd8f94
fix: try/except when iterating zarr children
alejoe91 Jul 15, 2026
253241a
ci: test python 3.14
alejoe91 Jul 16, 2026
b5bdfa2
ci: py3.13, one thing at a time
alejoe91 Jul 16, 2026
f064f20
fix: conflicts
alejoe91 Jul 30, 2026
a8bd627
fix: storage_options
alejoe91 Jul 30, 2026
78c10e2
build: update to python 3.11 in all actions
alejoe91 Jul 30, 2026
b162095
fix: convert channel_ids to StringDtype before saving to zarr
alejoe91 Jul 30, 2026
c82ea59
fix: load v2 array/objects compatibility and add SortingAnalyzer with…
alejoe91 Jul 31, 2026
64a145b
fix: cross serialization workflow includes dev
alejoe91 Jul 31, 2026
fc1a7ed
Merge branch 'main' into zarrv3
alejoe91 Jul 31, 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
57 changes: 49 additions & 8 deletions .github/scripts/serialization/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
the full state (traces or spike trains, properties, annotations, probe) to disk.
Targets the on-disk encoding axis: property and annotation preservation, and the
probe representation. "binary" is recording-only, hence "numpy_folder" for sortings.

Additionally, the `check_extra_data` kwarg can be used to store additional information in a JSON file alongside the
fixture, which is then passed to the check function. This is used for the SortingAnalyzer extension data keys,
which are computed and stored in the fixture JSON to verify that they are reloaded correctly.

"""
from packaging.version import parse
from spikeinterface import __version__ as si_version
Expand All @@ -31,6 +36,7 @@
"binary": "_binary",
"numpy_folder": "_numpy_folder",
"zarr": ".zarr",
"binary_folder": "_binary_folder",
}


Expand All @@ -49,7 +55,7 @@ def _build_noise_generator_recording():
return NoiseGeneratorRecording(num_channels=4, sampling_frequency=30000.0, durations=[1.0, 1.5], seed=0)


def _check_noise_generator_recording(rec):
def _check_noise_generator_recording(rec, check_extra_data=None):
assert type(rec).__name__ == "NoiseGeneratorRecording", type(rec).__name__
assert rec.get_num_channels() == 4
assert rec.get_num_segments() == 2
Expand All @@ -62,7 +68,7 @@ def _build_mock_recording():
return generate_recording(num_channels=4, durations=[1.0], sampling_frequency=30000.0, seed=0)


def _check_mock_recording(rec):
def _check_mock_recording(rec, check_extra_data=None):
from spikeinterface.core import BaseRecording

assert isinstance(rec, BaseRecording), type(rec).__name__
Expand All @@ -83,7 +89,7 @@ def _build_recording_with_properties():
return rec


def _check_recording_with_properties(rec):
def _check_recording_with_properties(rec, check_extra_data=None):
assert rec.get_num_channels() == 4
assert list(rec.get_property("quality")) == ["good", "good", "bad", "good"]
assert rec.get_annotation("experimenter") == "test"
Expand All @@ -104,7 +110,7 @@ def _build_recording_with_probe():
return rec_with_probe


def _check_recording_with_probe(rec):
def _check_recording_with_probe(rec, check_extra_data=None):
import numpy as np

assert rec.get_num_channels() == 8
Expand Down Expand Up @@ -137,7 +143,7 @@ def _build_recording_with_interleaved_probes():
return rec_with_probe


def _check_recording_with_interleaved_probes(rec):
def _check_recording_with_interleaved_probes(rec, check_extra_data=None):
import numpy as np

assert rec.get_num_channels() == 8
Expand Down Expand Up @@ -170,7 +176,7 @@ def _build_preprocessed_chain():
return common_reference(scale(rec, gain=2.0))


def _check_preprocessed_chain(rec):
def _check_preprocessed_chain(rec, check_extra_data=None):
# The outer wrapper and the recursive parent chain must both reload (the kwargs
# embed the parent recording dict, so this exercises recursive deserialization).
assert type(rec).__name__ == "CommonReferenceRecording", type(rec).__name__
Expand All @@ -184,7 +190,7 @@ def _build_sorting():
return generate_sorting(num_units=5, sampling_frequency=30000.0, durations=[1.0])


def _check_sorting(sorting):
def _check_sorting(sorting, check_extra_data=None):
assert sorting.get_num_units() == 5, sorting.get_num_units()
assert sorting.get_num_segments() == 1
spike_train = sorting.get_unit_spike_train(sorting.unit_ids[0], segment_index=0)
Expand All @@ -201,12 +207,41 @@ def _build_sorting_with_properties():
return sorting


def _check_sorting_with_properties(sorting):
def _check_sorting_with_properties(sorting, check_extra_data=None):
assert sorting.get_num_units() == 4
assert list(sorting.get_property("quality")) == ["good", "good", "bad", "good"]
assert sorting.get_annotation("experimenter") == "test"


def _build_sorting_analyzer_with_extensions():
from spikeinterface.core import generate_ground_truth_recording, create_sorting_analyzer
recording, sorting = generate_ground_truth_recording(durations=[10, 5], num_channels=16, num_units=5, seed=0)
analyzer = create_sorting_analyzer(sorting, recording)
extensions = analyzer.get_computable_extensions()
analyzer.compute(extensions, n_jobs=-1)
# for SortingAnalyzer, we also save a JSON file with extension as
# keys and date entries as values to check that everything is reloaded correctly
check_extra_data = {}
for ext_name in analyzer.extensions:
ext = analyzer.get_extension(ext_name)
check_extra_data[ext_name] = list(ext.data.keys())
return analyzer, check_extra_data


def _check_sorting_analyzer_with_extensions(analyzer, check_extra_data=None):
extensions = analyzer.get_saved_extension_names()
for extension in extensions:
extension = analyzer.get_extension(extension) # just check it loads without error
ext_data = extension.get_data()
assert ext_data is not None, f"extension {extension} returned None data"
if check_extra_data is not None:
# Check that the extension data keys match what was saved in the fixture JSON.
for ext_name, expected_keys in check_extra_data.items():
ext = analyzer.get_extension(ext_name)
actual_keys = set(ext.data.keys())
assert actual_keys == set(expected_keys), f"extension {ext_name} keys mismatch: {actual_keys} != {set(expected_keys)}"


OBJECTS = [
{
"id": "noise_generator_recording",
Expand Down Expand Up @@ -256,4 +291,10 @@ def _check_sorting_with_properties(sorting):
"check": _check_sorting_with_properties,
"formats": ["numpy_folder", "zarr"],
},
{
"id": "sorting_analyzer_with_extensions",
"build": _build_sorting_analyzer_with_extensions,
"check": _check_sorting_analyzer_with_extensions, # no check; the extensions are computed and stored, but the analyzer itself is not serialized
"formats": ["binary_folder", "zarr"],
},
]
47 changes: 34 additions & 13 deletions .github/scripts/serialization/serialize_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,52 @@
"""

import sys
import json
from pathlib import Path

import spikeinterface
from spikeinterface import __version__ as si_version
from spikeinterface import SortingAnalyzer
from spikeinterface.core.base import BaseExtractor


sys.path.insert(0, str(Path(__file__).parent))
from objects import OBJECTS, FIXTURE_SUFFIX # noqa: E402

out_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("serialization_fixtures")
out_dir.mkdir(parents=True, exist_ok=True) # do not rmtree an arbitrary path; overwrite in place

print(f"Generating serialization fixtures with spikeinterface {spikeinterface.__version__}")
print(f"Generating serialization fixtures with spikeinterface {si_version}")
for entry in OBJECTS:
obj = entry["build"]()
objs = entry["build"]()
if isinstance(objs, tuple) and len(objs) == 2 and isinstance(objs[1], dict):
obj, check_extra_data = objs
else:
obj = objs
check_extra_data = None
for fmt in entry["formats"]:
dest = out_dir / f"{entry['id']}{FIXTURE_SUFFIX[fmt]}"
if fmt == "json":
obj.dump_to_json(dest)
elif fmt == "pickle":
obj.dump_to_pickle(dest)
elif fmt == "binary":
obj.save(folder=dest, format="binary", overwrite=True)
elif fmt == "numpy_folder":
obj.save(folder=dest, format="numpy_folder", overwrite=True)
elif fmt == "zarr":
obj.save(folder=dest, format="zarr", overwrite=True)
if isinstance(obj, BaseExtractor):
if fmt == "json":
obj.dump_to_json(dest)
elif fmt == "pickle":
obj.dump_to_pickle(dest)
elif fmt == "binary":
obj.save(folder=dest, format="binary", overwrite=True)
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 isinstance(obj, SortingAnalyzer):
if dest.is_dir():
import shutil
shutil.rmtree(dest)
obj.save_as(folder=dest, format=fmt)

print(f" wrote {dest.name} ({fmt})")
if check_extra_data is not None:
json_dest = out_dir / f"{entry['id']}.json"
with open(json_dest, "w") as f:
json.dump(check_extra_data, f)
print(f" wrote {json_dest.name} (extra data)")

print(f"Fixtures written to: {out_dir.resolve()}")
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""

import os
import json
from pathlib import Path

import pytest
Expand All @@ -27,4 +28,8 @@ def test_load_old_serialized_object(entry, fmt):
fixture = FIXTURES_DIR / f"{entry['id']}{FIXTURE_SUFFIX[fmt]}"
assert fixture.exists(), f"missing fixture {fixture}"
obj = load(fixture)
entry["check"](obj)
check_extra_data = None
if (FIXTURES_DIR / f"{entry['id']}.json").exists():
with open(FIXTURES_DIR / f"{entry['id']}.json", "r") as f:
check_extra_data = json.load(f)
entry["check"](obj, check_extra_data=check_extra_data)
2 changes: 1 addition & 1 deletion .github/workflows/all-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.13"] # Lower and higher versions we support
python-version: ["3.11", "3.13"] # Lower and higher versions we support
os: [macos-latest, windows-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v6
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/caches_cron_job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
steps:
- uses: actions/setup-python@v6
with:
python-version: "3.10"
python-version: "3.11"
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.10"
python-version: "3.11"
enable-cache: true
ignore-nothing-to-cache: true # some runs do not require building a cache (ie macOS) this says this is okay
- name: Create the directory to store the data
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/core-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.10"
python-version: "3.11"
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.10"
python-version: "3.11"
enable-cache: true
- name: Install dependencies
run: |
Expand Down
31 changes: 23 additions & 8 deletions .github/workflows/cross_version_serialization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ name: Cross-version serialization
# 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.
# manual dispatch. The pseudo-version "dev" is always included: it generates the fixtures
# with the branch under test, so a round-trip break (write and read with the same code)
# is caught independently of any release.

on:
pull_request:
Expand Down Expand Up @@ -43,7 +45,7 @@ jobs:
steps:
- uses: actions/setup-python@v6
with:
python-version: "3.10"
python-version: "3.11"
- id: set
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
Expand Down Expand Up @@ -71,7 +73,8 @@ jobs:
if os.environ.get('GITHUB_EVENT_NAME') == 'pull_request':
minor_releases = minor_releases[-1:]

print(json.dumps([str(v) for v in minor_releases]))
# always round-trip against the branch itself as well
print(json.dumps([str(v) for v in minor_releases] + ['dev']))
")

echo "list=$versions_to_test" >> "$GITHUB_OUTPUT"
Expand All @@ -93,21 +96,33 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
python-version: "3.11"

- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.10"
python-version: "3.11"
enable-cache: false

- name: Generate fixtures with spikeinterface ${{ matrix.si-version }}
env:
SI_VERSION: ${{ matrix.si-version }}
run: |
uv run --isolated --no-project --python 3.10 \
--with "spikeinterface[core]==${{ matrix.si-version }}" \
# "dev" means the checked-out branch; anything else is a release from PyPI.
# [full] is needed because the SortingAnalyzer fixture computes every
# extension (scikit-learn, numba, ...).
if [ "$SI_VERSION" = "dev" ]; then
spec=".[full]"
else
spec="spikeinterface[full]==$SI_VERSION"
fi
uv run --isolated --no-project --python 3.11 \
--with "$spec" \
python .github/scripts/serialization/serialize_objects.py "$SI_SERIALIZATION_FIXTURES_DIR"

- name: Load fixtures with the branch
run: |
uv pip install --system -e . --group test-core
# [full] here too: loading the analyzer extensions unpickles e.g. the
# scikit-learn PCA models written at generation time
uv pip install --system -e ".[full]" --group test-core
pytest .github/scripts/serialization/test_cross_version_compatibility.py -v
10 changes: 4 additions & 6 deletions .github/workflows/deepinterpolation.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
name: Testing deepinterpolation

# Manual only — deepinterpolation requires Python 3.11, incompatible with 3.11+ required by Zarr 3.0.0+
on:
pull_request:
types: [synchronize, opened, reopened]
branches:
- main
workflow_dispatch:

concurrency: # Cancel previous workflows on the same pull request
group: ${{ github.workflow }}-${{ github.ref }}
Expand All @@ -22,10 +20,10 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.10"
python-version: "3.11"
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.10"
python-version: "3.11"
enable-cache: false
- name: Get changed files
id: changed-files
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test_containers_docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.10"
python-version: '3.11'
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.10"
python-version: '3.11'
enable-cache: true
- name: Python version
run: python --version
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test_containers_singularity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.10"
python-version: '3.11'
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.10"
python-version: '3.11'
enable-cache: true
- uses: eWaterCycle/setup-singularity@v7
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test_imports.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.10"
python-version: "3.11"
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.10"
python-version: "3.11"
enable-cache: true
- name: Install Spikeinterface with only core dependencies
run: |
Expand Down
Loading
Loading