Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3dc5729
Test IBL extractors tests failing for PI update
alejoe91 Dec 29, 2025
d1a0532
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Jan 6, 2026
33c6769
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Jan 16, 2026
2c94bac
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Jan 20, 2026
a40d073
Merge branch 'main' of github.com:alejoe91/spikeinterface
alejoe91 Feb 24, 2026
ef40b73
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 17, 2026
11c5812
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 24, 2026
ada53f8
Merge branch 'main' of github.com:SpikeInterface/spikeinterface
alejoe91 Mar 24, 2026
845ea33
Use ProbeGroup object instead of contact_vector
alejoe91 Mar 24, 2026
1426bf8
Apply suggestion from @alejoe91
alejoe91 Mar 24, 2026
c2dbeaf
Apply suggestions from code review
alejoe91 Mar 24, 2026
fa426d9
Merge branch 'main' into probegroup
alejoe91 Mar 25, 2026
15331e5
Remove contact vector from extractors/sortingcomponents
alejoe91 Mar 25, 2026
4ccb318
fix: update test_interpolate_bad_channels probe manipulation
alejoe91 Mar 25, 2026
485a354
test: remove 'location' from IBL properties check
alejoe91 Mar 25, 2026
4d2c56f
fix: extra_metadata not used in copy_metadata if only_main=True
alejoe91 Mar 25, 2026
dd26548
Fix dtype issue in average_across_directions
alejoe91 Mar 25, 2026
6d31906
Clean up backward-compatibility
alejoe91 Apr 16, 2026
db357a0
fix annotations
alejoe91 Apr 16, 2026
bf5a1a4
fix ibl tests
alejoe91 Apr 16, 2026
e1ea673
fix: conflicts
alejoe91 Jun 29, 2026
f712b34
refac: modify set_probe and add select_channels_with_probe
alejoe91 Jun 29, 2026
b907d73
test: fix backward compatibility test
alejoe91 Jun 29, 2026
68735fe
oups
alejoe91 Jun 29, 2026
0e3a0bd
fix: most tests
alejoe91 Jun 29, 2026
6c1ff79
test: fix bacward compat tests
alejoe91 Jun 29, 2026
c749189
docs: fix doc tests
alejoe91 Jun 29, 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
96 changes: 96 additions & 0 deletions .github/scripts/create_probe_compat_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env python
"""
Creates probe compatibility fixtures using the *currently installed* spikeinterface.

Run this script with spikeinterface==0.104.* installed to produce the fixture
files consumed by test_probe_backward_compat.py:

python create_probe_compat_fixtures.py [output_dir]

If output_dir is omitted, fixtures are written to ./probe_compat_fixtures.
"""

import sys
import shutil
import numpy as np
from pathlib import Path

import spikeinterface

print(f"Creating fixtures with spikeinterface {spikeinterface.__version__}")

OUTPUT_DIR = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("probe_compat_fixtures")
if OUTPUT_DIR.exists():
shutil.rmtree(OUTPUT_DIR)
OUTPUT_DIR.mkdir(parents=True)

from probeinterface import generate_linear_probe, ProbeGroup
from spikeinterface.core import NumpyRecording

# -----------------------------------------------------------------------
# Fixture 1: single probe, sequential device_channel_indices
# -----------------------------------------------------------------------
n = 8
probe = generate_linear_probe(num_elec=n, ypitch=20.0)
probe.annotate(name="test_probe", manufacturer="test_vendor")
probe.set_contact_ids([f"e{i}" for i in range(n)])
probe.set_device_channel_indices(np.arange(n))
probe.create_auto_shape()

traces = np.arange(1000 * n, dtype="int16").reshape(1000, n)
rec_single = NumpyRecording([traces], sampling_frequency=30000.0)
rec_single = rec_single.set_probe(probe) # old API: in_place=False, returns new recording

rec_single.save(folder=str(OUTPUT_DIR / "single_probe_binary"))
rec_single.save(folder=str(OUTPUT_DIR / "single_probe.zarr"), format="zarr")

# -----------------------------------------------------------------------
# Fixture 2: two probes with per-probe name/manufacturer
# -----------------------------------------------------------------------
n_A, n_B = 8, 8
probe_A = generate_linear_probe(num_elec=n_A, ypitch=20.0)
probe_A.move([0.0, 0.0])
probe_A.annotate(name="probe_A", manufacturer="vendor_X")
probe_A.set_contact_ids([f"a{i}" for i in range(n_A)])
probe_A.set_device_channel_indices(np.arange(n_A))
probe_A.create_auto_shape()

probe_B = generate_linear_probe(num_elec=n_B, ypitch=20.0)
probe_B.move([500.0, 0.0])
probe_B.annotate(name="probe_B", manufacturer="vendor_Y")
probe_B.set_contact_ids([f"b{i}" for i in range(n_B)])
probe_B.set_device_channel_indices(np.arange(n_A, n_A + n_B))
probe_B.create_auto_shape()

pg = ProbeGroup()
pg.add_probe(probe_A)
pg.add_probe(probe_B)

n_total = n_A + n_B
traces2 = np.arange(1000 * n_total, dtype="int16").reshape(1000, n_total)
rec_two = NumpyRecording([traces2], sampling_frequency=30000.0)
rec_two = rec_two.set_probegroup(pg) # old API: in_place=False, returns new recording

rec_two.save(folder=str(OUTPUT_DIR / "two_probe_binary"))
rec_two.save(folder=str(OUTPUT_DIR / "two_probe.zarr"), format="zarr")

# -----------------------------------------------------------------------
# Fixture 3: probe with shuffled device_channel_indices
# Verifies that the channel-reordering logic is preserved across versions.
# -----------------------------------------------------------------------
n = 8
probe_sh = generate_linear_probe(num_elec=n, ypitch=20.0)
probe_sh.annotate(name="shuffled_probe", manufacturer="shuffle_vendor")
shuffled_dci = np.array([3, 0, 7, 1, 5, 2, 6, 4]) # permutation of 0..7
probe_sh.set_device_channel_indices(shuffled_dci)

# traces[:, j] corresponds to recording channel j, which after set_probe
# is mapped to the contact whose dci equals j.
traces3 = np.arange(1000 * n, dtype="int16").reshape(1000, n)
rec_sh = NumpyRecording([traces3], sampling_frequency=30000.0)
rec_sh = rec_sh.set_probe(probe_sh) # old API

rec_sh.save(folder=str(OUTPUT_DIR / "shuffled_probe_binary"))
rec_sh.save(folder=str(OUTPUT_DIR / "shuffled_probe.zarr"), format="zarr")

print(f"Fixtures written to: {OUTPUT_DIR.resolve()}")
51 changes: 51 additions & 0 deletions .github/workflows/probe_backward_compat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Probe backward compatibility

on:
pull_request:
types: [synchronize, opened, reopened]
branches:
- main
paths:
- 'src/spikeinterface/core/**'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
probe-backward-compat:
name: Probe compat (SI 0.104.* → current)
runs-on: ubuntu-latest
env:
SI_PROBE_COMPAT_FIXTURES_DIR: ${{ github.workspace }}/probe_compat_fixtures

steps:
- name: Check out code
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'

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

# Step 1: install the OLD release and create fixtures.
# The fixture script uses the old in_place=False default (returns a new recording),
# saves to binary folder + JSON, and writes a known probe name/manufacturer/contact_ids.
- name: Install spikeinterface 0.104.* to create fixtures
run: uv pip install --system "spikeinterface[core]==0.104.*"

- name: Create compatibility fixtures with old version
run: python .github/scripts/create_probe_compat_fixtures.py "$SI_PROBE_COMPAT_FIXTURES_DIR"

# Step 2: install the NEW version from this PR source and run the load tests.
- name: Install new spikeinterface from source
run: uv pip install --system -e . --group test-core

- name: Run backward compatibility tests
run: pytest src/spikeinterface/core/tests/test_probe_backward_compat.py -v
8 changes: 8 additions & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ spikeinterface.core
.. automethod:: BaseRecording.dump_to_json
.. automethod:: BaseRecording.dump_to_pickle
.. automethod:: BaseRecording.remove_channels
.. automethod:: BaseRecording.set_probe
.. automethod:: BaseRecording.set_probegroup
.. automethod:: BaseRecording.reset_probe
.. automethod:: BaseRecording.select_channels_with_probe
.. automethod:: BaseRecording.select_channels_with_probegroup
.. automethod:: BaseRecording.split_by
.. autoclass:: BaseSorting
:members:
.. automethod:: BaseSorting.save
Expand All @@ -25,6 +31,8 @@ spikeinterface.core
.. automethod:: BaseSorting.dump
.. automethod:: BaseSorting.dump_to_json
.. automethod:: BaseSorting.dump_to_pickle
.. automethod:: BaseSorting.split_by
.. automethod:: BaseSorting.register_recording
.. autoclass:: BaseSnippets
:members:
.. automethod:: BaseSnippets.save
Expand Down
5 changes: 3 additions & 2 deletions doc/get_started/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,9 @@ to set it *manually*.


If your recording does not have a ``Probe``, you can set it using
``set_probe``. Note: ``set_probe`` creates a copy of the recording with
the new probe, rather than modifying the existing recording in place.
``set_probe``. Note: ``set_probe`` modifies the recording in place. To
get a new recording object with a subset of channels attached to a probe,
use ``select_channels_with_probe``.
There is more information
`here <https://spikeinterface.readthedocs.io/en/latest/modules_gallery/core/plot_3_handle_probe_info.html>`__.

Expand Down
18 changes: 12 additions & 6 deletions doc/modules/core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -522,12 +522,18 @@ The probe has 4 shanks, which can be loaded as separate groups (and spike sorted
# add wiring
probe.wiring_to_device('ASSY-156>RHD2164')

# set probe
recording_w_probe = recording.set_probe(probe)
# set probe with group info and return a new recording object
recording_w_probe = recording.set_probe(probe, group_mode="by_shank")
# set probe in place, ie, modify the current recording
recording.set_probe(probe, group_mode="by_shank", in_place=True)
# set probe (modifies the recording in place)
recording.set_probe(probe)
# set probe with group info derived from shank ids (in place)
recording.set_probe(probe, group_mode="by_shank")

# to get a *new* recording without modifying the original, use select_channels_with_probe
recording_w_probe = recording.select_channels_with_probe(probe)
recording_w_probe = recording.select_channels_with_probe(probe, group_mode="by_shank")

# multi-probe recordings use set_probegroup / select_channels_with_probegroup
recording.set_probegroup(probegroup)
recording_w_probegroup = recording.select_channels_with_probegroup(probegroup)

# retrieve probe
probe_from_recording = recording.get_probe()
Expand Down
6 changes: 3 additions & 3 deletions examples/forhowto/plot_working_with_tetrodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@
# We can now attach the :code:`tetrode_group` to our recording. To check if this worked, we'll
# plot the probe map

recording_with_probe = recording.set_probegroup(tetrode_group)
plot_probe_map(recording_with_probe)
recording.set_probegroup(tetrode_group)
plot_probe_map(recording)

##############################################################################
# Looks good! Now that the recording is aware of the probe geometry, we can
# begin a standard spike sorting pipeline. First, we can apply preprocessing.
# Note that we apply this preprocessing on the entire bundle of tetrodes.

preprocessed_recording = spre.bandpass_filter(recording_with_probe)
preprocessed_recording = spre.bandpass_filter(recording)

##############################################################################
# WARNING: a very common preprocessing step is to apply a common median
Expand Down
4 changes: 2 additions & 2 deletions examples/get_started/quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@
# -

# If your recording does not have a `Probe`, you can set it using `set_probe`.
# Note: `set_probe` creates a copy of the recording with the new probe,
# rather than modifying the existing recording in place.
# Note: `set_probe` modifies the recording in place. To get a new recording
# object with a subset of channels attached to a probe, use `select_channels_with_probe`.
# There is more information [here](https://spikeinterface.readthedocs.io/en/latest/modules_gallery/core/plot_3_handle_probe_info.html).

# Using the `spikeinterface.preprocessing` module, you can perform preprocessing on the recordings.
Expand Down
2 changes: 1 addition & 1 deletion examples/tutorials/core/plot_1_recording_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
probe.set_device_channel_indices(np.arange(7))

# then we need to actually set the probe to the recording object
recording = recording.set_probe(probe)
recording.set_probe(probe)
plot_probe(probe)

##############################################################################
Expand Down
10 changes: 5 additions & 5 deletions examples/tutorials/core/plot_3_handle_probe_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
print(other_probe)

other_probe.set_device_channel_indices(np.arange(32))
recording_2_shanks = recording.set_probe(other_probe, group_mode="by_shank")
plot_probe(recording_2_shanks.get_probe())
recording.set_probe(other_probe, group_mode="by_shank")
plot_probe(recording.get_probe())

###############################################################################
# Now let's check what we have loaded. The :code:`group_mode='by_shank'` automatically
Expand All @@ -53,11 +53,11 @@
# We can access this information either as a dict with :code:`outputs='dict'` (default)
# or as a list of recordings with :code:`outputs='list'`.

print(recording_2_shanks)
print(f'\nGroup Property: {recording_2_shanks.get_property("group")}\n')
print(recording)
print(f'\nGroup Property: {recording.get_property("group")}\n')

# Here we split as a dict
sub_recording_dict = recording_2_shanks.split_by(property="group", outputs='dict')
sub_recording_dict = recording.split_by(property="group", outputs='dict')

# Then we can pull out the individual sub-recordings
sub_rec0 = sub_recording_dict[0]
Expand Down
55 changes: 51 additions & 4 deletions src/spikeinterface/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,14 @@ def id_to_index(self, id) -> int:
return ind

def annotate(self, **new_annotations) -> None:
"""Adds annotations.

Parameters
----------
**new_annotations : dict
Key-value pairs of annotations to add. If an annotation key already exists,
it will be overwritten.
"""
self._annotations.update(new_annotations)

def set_annotation(self, annotation_key: str, value: Any, overwrite=False) -> None:
Expand All @@ -243,6 +251,24 @@ def set_annotation(self, annotation_key: str, value: Any, overwrite=False) -> No
else:
raise ValueError(f"{annotation_key} is already an annotation key. Use 'overwrite=True' to overwrite it")

def delete_annotation(self, annotation_key: str) -> None:
"""Deletes existing annotation.

Parameters
----------
annotation_key : str
The annotation key to delete

Raises
------
ValueError
If the annotation key does not exist
"""
if annotation_key in self._annotations.keys():
del self._annotations[annotation_key]
else:
raise ValueError(f"{annotation_key} is not an annotation key")

def get_preferred_mp_context(self):
"""
Get the preferred context for multiprocessing.
Expand Down Expand Up @@ -441,6 +467,15 @@ def copy_metadata(
if self._preferred_mp_context is not None:
other._preferred_mp_context = self._preferred_mp_context

if not only_main:
self._extra_metadata_copy(other)

def _extra_metadata_copy(self, other: "BaseExtractor") -> None:
"""
This is a hook to copy extra metadata that is not in the annotations/properties dict.
"""
pass

def to_dict(
self,
include_annotations: bool = False,
Expand Down Expand Up @@ -574,6 +609,8 @@ def to_dict(
folder_metadata = Path(folder_metadata).resolve().absolute().relative_to(relative_to)
dump_dict["folder_metadata"] = str(folder_metadata)

self._extra_metadata_to_dict(dump_dict)

return dump_dict

@staticmethod
Expand Down Expand Up @@ -610,8 +647,6 @@ def load_metadata_from_folder(self, folder_metadata):
# hack to load probe for recording
folder_metadata = Path(folder_metadata)

self._extra_metadata_from_folder(folder_metadata)

# load properties
prop_folder = folder_metadata / "properties"
if prop_folder.is_dir():
Expand All @@ -621,6 +656,8 @@ def load_metadata_from_folder(self, folder_metadata):
key = prop_file.stem
self.set_property(key, values)

self._extra_metadata_from_folder(folder_metadata)

def save_metadata_to_folder(self, folder_metadata):
self._extra_metadata_to_folder(folder_metadata)

Expand Down Expand Up @@ -862,6 +899,14 @@ def _extra_metadata_to_folder(self, folder):
# This implemented in BaseRecording for probe
pass

def _extra_metadata_from_dict(self, dump_dict):
# This implemented in BaseRecording for probe
pass

def _extra_metadata_to_dict(self, dump_dict):
# This implemented in BaseRecording for probe
pass

def save(self, **kwargs) -> "BaseExtractor":
"""
Save a SpikeInterface object.
Expand Down Expand Up @@ -997,10 +1042,10 @@ def save_to_folder(
else:
warnings.warn("The extractor is not serializable to file. The provenance will not be saved.")

self.save_metadata_to_folder(folder)

# save data (done the subclass)
self.save_metadata_to_folder(folder)
cached = self._save(folder=folder, verbose=verbose, **save_kwargs)
cached.load_metadata_from_folder(folder)

# copy properties/
self.copy_metadata(cached)
Expand Down Expand Up @@ -1155,6 +1200,8 @@ def _load_extractor_from_dict(dic) -> "BaseExtractor":
for k, v in dic["properties"].items():
extractor.set_property(k, v)

extractor._extra_metadata_from_dict(dic)

return extractor


Expand Down
Loading
Loading