Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 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
d582737
Merge branch 'main' into probegroup
h-mayorquin Jul 1, 2026
6c627f2
fix: conflicts
alejoe91 Jul 2, 2026
bf88571
fix: code review from sam and extedn backward compatibility tests
alejoe91 Jul 2, 2026
77b2c39
fix: rename reset_probe and remove legacy test
alejoe91 Jul 2, 2026
4baf4af
test: remove 0.101.* version
alejoe91 Jul 2, 2026
76276a2
chris' suggestion
alejoe91 Jul 3, 2026
5c9932f
test: add test on interleaved probes
alejoe91 Jul 3, 2026
ac3ec22
small stuff
samuelgarcia Jul 3, 2026
72ac5ce
fix: add check for unique locations to BaseSorter
alejoe91 Jul 3, 2026
9686804
Merge branch 'probegroup' of github.com:alejoe91/spikeinterface into …
alejoe91 Jul 3, 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
137 changes: 137 additions & 0 deletions .github/scripts/create_probe_compat_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/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.

Note: we use `in_place=True` since a bug (fixed in #4300) prevented probes_info to be properly
saved as annotations in the probegroup when using `in_place=False` in spikeinterface 0.104.*.
"""

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.set_probe(probe, in_place=True)

rec_single_bin = rec_single.save(folder=str(OUTPUT_DIR / "single_probe_binary"))
rec_single_zarr = rec_single.save(folder=str(OUTPUT_DIR / "single_probe.zarr"), format="zarr")
rec_single_bin.dump_to_json(str(OUTPUT_DIR / "single_probe.json"))
rec_single_bin.dump_to_pickle(str(OUTPUT_DIR / "single_probe.pkl"))

# -----------------------------------------------------------------------
# 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.set_probegroup(pg, in_place=True)

rec_two_bin = rec_two.save(folder=str(OUTPUT_DIR / "two_probe_binary"))
rec_two_zarr = rec_two.save(folder=str(OUTPUT_DIR / "two_probe.zarr"), format="zarr")
rec_two_bin.dump_to_json(str(OUTPUT_DIR / "two_probe.json"))
rec_two_bin.dump_to_pickle(str(OUTPUT_DIR / "two_probe.pkl"))

# -----------------------------------------------------------------------
# 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.set_probe(probe_sh, in_place=True)

rec_sh_bin = rec_sh.save(folder=str(OUTPUT_DIR / "shuffled_probe_binary"))
rec_sh_zarr = rec_sh.save(folder=str(OUTPUT_DIR / "shuffled_probe.zarr"), format="zarr")
rec_sh_bin.dump_to_json(str(OUTPUT_DIR / "shuffled_probe.json"))
rec_sh_bin.dump_to_pickle(str(OUTPUT_DIR / "shuffled_probe.pkl"))

print(f"Fixtures written to: {OUTPUT_DIR.resolve()}")

# -----------------------------------------------------------------------
# Fixture 4: two probes with interleaved device_channel_indices
# -----------------------------------------------------------------------
n = 8
probe_A = generate_linear_probe(num_elec=n, 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)])
probe_A.set_device_channel_indices(np.arange(0, 2 * n, 2)) # even indices
probe_A.create_auto_shape()

probe_B = generate_linear_probe(num_elec=n, 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)])
probe_B.set_device_channel_indices(np.arange(1, 2 * n, 2)) # odd indices
probe_B.create_auto_shape()

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

n_total = 2 * n
traces2 = np.arange(1000 * n_total, dtype="int16").reshape(1000, n_total)
rec_two_inter = NumpyRecording([traces2], sampling_frequency=30000.0)
rec_two_inter.set_probegroup(pg, in_place=True)

rec_two_inter_bin = rec_two_inter.save(folder=str(OUTPUT_DIR / "two_probe_interleaved_binary"))
rec_two_inter_zarr = rec_two_inter.save(folder=str(OUTPUT_DIR / "two_probe_interleaved.zarr"), format="zarr")
rec_two_inter_bin.dump_to_json(str(OUTPUT_DIR / "two_probe_interleaved.json"))
rec_two_inter_bin.dump_to_pickle(str(OUTPUT_DIR / "two_probe_interleaved.pkl"))
58 changes: 58 additions & 0 deletions .github/workflows/probe_backward_compat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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 ${{ matrix.si-version }} → current)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
si-version:
- '0.102.*'
- '0.103.*'
- '0.104.*'
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 ${{ matrix.si-version }} to create fixtures
run: uv pip install --system "spikeinterface[core]==${{ matrix.si-version }}"

- 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
Loading
Loading