Skip to content

Commit 74ebd22

Browse files
committed
Merge branch 'main' into add_distance_function
2 parents a082543 + d9531ab commit 74ebd22

11 files changed

Lines changed: 196 additions & 64 deletions

File tree

.github/workflows/full-test-with-codecov.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ jobs:
5252
- name: Shows installed packages by pip, git-annex and cached testing files
5353
uses: ./.github/actions/show-test-environment
5454
- name: run tests
55+
env:
56+
HDF5_PLUGIN_PATH: ${{ github.workspace }}/hdf5_plugin_path_maxwell
5557
run: |
5658
source ${{ github.workspace }}/test_env/bin/activate
5759
pytest -m "not sorters_external" --cov=./ --cov-report xml:./coverage.xml -vv -ra --durations=0 | tee report_full.txt; test ${PIPESTATUS[0]} -eq 0 || exit 1

.github/workflows/full-test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ jobs:
132132
- name: Test core
133133
run: ./.github/run_tests.sh core
134134
- name: Test extractors
135+
env:
136+
HDF5_PLUGIN_PATH: ${{ github.workspace }}/hdf5_plugin_path_maxwell
135137
if: ${{ steps.modules-changed.outputs.EXTRACTORS_CHANGED == 'true' || steps.modules-changed.outputs.CORE_CHANGED == 'true' }}
136138
run: ./.github/run_tests.sh "extractors and not streaming_extractors"
137139
- name: Test preprocessing

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ With SpikeInterface, users can:
6767

6868
## Documentation
6969

70-
Detailed documentation of the latest PyPI release of SpikeInterface can be found [here](https://spikeinterface.readthedocs.io/en/0.99.0).
70+
Detailed documentation of the latest PyPI release of SpikeInterface can be found [here](https://spikeinterface.readthedocs.io/en/0.99.1).
7171

7272
Detailed documentation of the development version of SpikeInterface can be found [here](https://spikeinterface.readthedocs.io/en/latest).
7373

doc/releases/0.99.1.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.. _release0.99.1:
2+
3+
SpikeInterface 0.99.1 release notes
4+
-----------------------------------
5+
6+
14th November 2023
7+
8+
Minor release with some bug fixes.
9+
10+
* Fix crash when default start / end frame arguments on motion interpolation are used (#2176)
11+
* Fix bug in `make_match_count_matrix()` when computing matching events (#2182, #2191, #2196)
12+
* Fix maxwell tests by setting HDF5_PLUGIN_PATH env in action (#2161)
13+
* Add read_npz_sorting to extractors module (#2183)

doc/whatisnew.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Release notes
88
.. toctree::
99
:maxdepth: 1
1010

11+
releases/0.99.1.rst
1112
releases/0.99.0.rst
1213
releases/0.98.2.rst
1314
releases/0.98.1.rst
@@ -32,6 +33,12 @@ Release notes
3233
releases/0.9.1.rst
3334

3435

36+
Version 0.99.1
37+
==============
38+
39+
* Minor release with some bug fixes
40+
41+
3542
Version 0.99.0
3643
==============
3744

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "spikeinterface"
3-
version = "0.100.0.dev0"
3+
version = "0.99.1"
44
authors = [
55
{ name="Alessio Buccino", email="alessiop.buccino@gmail.com" },
66
{ name="Samuel Garcia", email="sam.garcia.die@gmail.com" },
@@ -146,8 +146,8 @@ test = [
146146

147147
# for github test : probeinterface and neo from master
148148
# for release we need pypi, so this need to be commented
149-
"probeinterface @ git+https://github.com/SpikeInterface/probeinterface.git",
150-
"neo @ git+https://github.com/NeuralEnsemble/python-neo.git",
149+
# "probeinterface @ git+https://github.com/SpikeInterface/probeinterface.git",
150+
# "neo @ git+https://github.com/NeuralEnsemble/python-neo.git",
151151
]
152152

153153
docs = [
@@ -164,8 +164,8 @@ docs = [
164164
"hdbscan>=0.8.33", # For sorters spykingcircus2 + tridesclous
165165
"numba", # For many postprocessing functions
166166
# for release we need pypi, so this needs to be commented
167-
"probeinterface @ git+https://github.com/SpikeInterface/probeinterface.git", # We always build from the latest version
168-
"neo @ git+https://github.com/NeuralEnsemble/python-neo.git", # We always build from the latest version
167+
# "probeinterface @ git+https://github.com/SpikeInterface/probeinterface.git", # We always build from the latest version
168+
# "neo @ git+https://github.com/NeuralEnsemble/python-neo.git", # We always build from the latest version
169169

170170
]
171171

src/spikeinterface/comparison/comparisontools.py

Lines changed: 96 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ def compute_agreement_score(num_matches, num1, num2):
8686
def do_count_event(sorting):
8787
"""
8888
Count event for each units in a sorting.
89+
90+
Kept for backward compatibility sorting.count_num_spikes_per_unit() is doing the same.
91+
8992
Parameters
9093
----------
9194
sorting: SortingExtractor
@@ -98,14 +101,7 @@ def do_count_event(sorting):
98101
"""
99102
import pandas as pd
100103

101-
unit_ids = sorting.get_unit_ids()
102-
ev_counts = np.zeros(len(unit_ids), dtype="int64")
103-
for segment_index in range(sorting.get_num_segments()):
104-
ev_counts += np.array(
105-
[len(sorting.get_unit_spike_train(u, segment_index=segment_index)) for u in unit_ids], dtype="int64"
106-
)
107-
event_counts = pd.Series(ev_counts, index=unit_ids)
108-
return event_counts
104+
return pd.Series(sorting.count_num_spikes_per_unit())
109105

110106

111107
def get_optimized_dot_product_function():
@@ -328,11 +324,9 @@ def compute_matching_matrix(
328324
delta_frames,
329325
):
330326
"""
331-
Compute a matrix representing the matches between two spike trains.
332-
333-
Given two spike trains, this function finds matching spikes based on a temporal proximity criterion
334-
defined by `delta_frames`. The resulting matrix indicates the number of matches between units
335-
in `spike_frames_train1` and `spike_frames_train2`.
327+
Internal function used by `make_match_count_matrix()`.
328+
This function is for one segment only.
329+
The loop over segment is done in `make_match_count_matrix()`
336330
337331
Parameters
338332
----------
@@ -359,31 +353,9 @@ def compute_matching_matrix(
359353
A 2D numpy array of shape `(num_units_train1, num_units_train2)`. Each element `[i, j]` represents
360354
the count of matching spike pairs between unit `i` from `spike_frames_train1` and unit `j` from `spike_frames_train2`.
361355
362-
363-
Notes
364-
-----
365-
This algorithm identifies matching spikes between two ordered spike trains.
366-
By iterating through each spike in the first train, it compares them against spikes in the second train,
367-
determining matches based on the two spikes frames being within `delta_frames` of each other.
368-
369-
To avoid redundant comparisons the algorithm maintains a reference, `second_train_search_start `,
370-
which signifies the minimal index in the second spike train that might match the upcoming spike
371-
in the first train.
372-
373-
The logic can be summarized as follows:
374-
1. Iterate through each spike in the first train
375-
2. For each spike, find the first match in the second train.
376-
3. Save the index of the first match as the new `second_train_search_start `
377-
3. For each match, find as many matches as possible from the first match onwards.
378-
379-
An important condition here is that the same spike is not matched twice. This is managed by keeping track
380-
of the last matched frame for each unit pair in `last_match_frame1` and `last_match_frame2`
381-
382-
For more details on the rationale behind this approach, refer to the documentation of this module and/or
383-
the metrics section in SpikeForest documentation.
384356
"""
385357

386-
matching_matrix = np.zeros((num_units_train1, num_units_train2), dtype=np.uint16)
358+
matching_matrix = np.zeros((num_units_train1, num_units_train2), dtype=np.uint64)
387359

388360
# Used to avoid the same spike matching twice
389361
last_match_frame1 = -np.ones_like(matching_matrix, dtype=np.int64)
@@ -411,11 +383,11 @@ def compute_matching_matrix(
411383
unit_index1, unit_index2 = unit_indices1[index1], unit_indices2[index2]
412384

413385
if (
414-
frame1 != last_match_frame1[unit_index1, unit_index2]
415-
and frame2 != last_match_frame2[unit_index1, unit_index2]
386+
index1 != last_match_frame1[unit_index1, unit_index2]
387+
and index2 != last_match_frame2[unit_index1, unit_index2]
416388
):
417-
last_match_frame1[unit_index1, unit_index2] = frame1
418-
last_match_frame2[unit_index1, unit_index2] = frame2
389+
last_match_frame1[unit_index1, unit_index2] = index1
390+
last_match_frame2[unit_index1, unit_index2] = index2
419391

420392
matching_matrix[unit_index1, unit_index2] += 1
421393

@@ -427,10 +399,65 @@ def compute_matching_matrix(
427399
return compute_matching_matrix
428400

429401

430-
def make_match_count_matrix(sorting1, sorting2, delta_frames):
402+
def make_match_count_matrix(sorting1, sorting2, delta_frames, ensure_symmetry=False):
403+
"""
404+
Computes a matrix representing the matches between two Sorting objects.
405+
406+
Given two spike trains, this function finds matching spikes based on a temporal proximity criterion
407+
defined by `delta_frames`. The resulting matrix indicates the number of matches between units
408+
in `spike_frames_train1` and `spike_frames_train2` for each pair of units.
409+
410+
Note that this algo is not symmetric and is biased with `sorting1` representing ground truth for the comparison
411+
412+
Parameters
413+
----------
414+
sorting1 : Sorting
415+
An array of integer frame numbers corresponding to spike times for the first train. Must be in ascending order.
416+
sorting2 : Sorting
417+
An array of integer frame numbers corresponding to spike times for the second train. Must be in ascending order.
418+
delta_frames : int
419+
The inclusive upper limit on the frame difference for which two spikes are considered matching. That is
420+
if `abs(spike_frames_train1[i] - spike_frames_train2[j]) <= delta_frames` then the spikes at
421+
`spike_frames_train1[i]` and `spike_frames_train2[j]` are considered matching.
422+
ensure_symmetry: bool, default False
423+
If ensure_symmetry=True, then the algo is run two times by switching sorting1 and sorting2.
424+
And the minimum of the two results is taken.
425+
Returns
426+
-------
427+
matching_matrix : ndarray
428+
A 2D numpy array of shape `(num_units_train1, num_units_train2)`. Each element `[i, j]` represents
429+
the count of matching spike pairs between unit `i` from `spike_frames_train1` and unit `j` from `spike_frames_train2`.
430+
431+
Notes
432+
-----
433+
This algorithm identifies matching spikes between two ordered spike trains.
434+
By iterating through each spike in the first train, it compares them against spikes in the second train,
435+
determining matches based on the two spikes frames being within `delta_frames` of each other.
436+
437+
To avoid redundant comparisons the algorithm maintains a reference, `second_train_search_start `,
438+
which signifies the minimal index in the second spike train that might match the upcoming spike
439+
in the first train.
440+
441+
The logic can be summarized as follows:
442+
1. Iterate through each spike in the first train
443+
2. For each spike, find the first match in the second train.
444+
3. Save the index of the first match as the new `second_train_search_start `
445+
3. For each match, find as many matches as possible from the first match onwards.
446+
447+
An important condition here is that the same spike is not matched twice. This is managed by keeping track
448+
of the last matched frame for each unit pair in `last_match_frame1` and `last_match_frame2`
449+
There are corner cases where a spike can be counted twice in the spiketrain 2 if there are bouts of bursting activity
450+
(below delta_frames) in the spiketrain 1. To ensure that the number of matches does not exceed the number of spikes,
451+
we apply a final clip.
452+
453+
454+
For more details on the rationale behind this approach, refer to the documentation of this module and/or
455+
the metrics section in SpikeForest documentation.
456+
"""
457+
431458
num_units_sorting1 = sorting1.get_num_units()
432459
num_units_sorting2 = sorting2.get_num_units()
433-
matching_matrix = np.zeros((num_units_sorting1, num_units_sorting2), dtype=np.uint16)
460+
matching_matrix = np.zeros((num_units_sorting1, num_units_sorting2), dtype=np.uint64)
434461

435462
spike_vector1_segments = sorting1.to_spike_vector(concatenated=False)
436463
spike_vector2_segments = sorting2.to_spike_vector(concatenated=False)
@@ -452,7 +479,7 @@ def make_match_count_matrix(sorting1, sorting2, delta_frames):
452479
unit_indices1_sorted = spike_vector1["unit_index"]
453480
unit_indices2_sorted = spike_vector2["unit_index"]
454481

455-
matching_matrix += get_optimized_compute_matching_matrix()(
482+
matching_matrix_seg = get_optimized_compute_matching_matrix()(
456483
sample_frames1_sorted,
457484
sample_frames2_sorted,
458485
unit_indices1_sorted,
@@ -462,6 +489,26 @@ def make_match_count_matrix(sorting1, sorting2, delta_frames):
462489
delta_frames,
463490
)
464491

492+
if ensure_symmetry:
493+
matching_matrix_seg_switch = get_optimized_compute_matching_matrix()(
494+
sample_frames2_sorted,
495+
sample_frames1_sorted,
496+
unit_indices2_sorted,
497+
unit_indices1_sorted,
498+
num_units_sorting2,
499+
num_units_sorting1,
500+
delta_frames,
501+
)
502+
matching_matrix_seg = np.maximum(matching_matrix_seg, matching_matrix_seg_switch.T)
503+
504+
matching_matrix += matching_matrix_seg
505+
506+
# ensure the number of match do not exceed the number of spike in train 2
507+
# this is a simple way to handle corner cases for bursting in sorting1
508+
spike_count2 = np.array(list(sorting2.count_num_spikes_per_unit().values()))
509+
spike_count2 = spike_count2[np.newaxis, :]
510+
matching_matrix = np.clip(matching_matrix, None, spike_count2)
511+
465512
# Build a data frame from the matching matrix
466513
import pandas as pd
467514

@@ -472,12 +519,12 @@ def make_match_count_matrix(sorting1, sorting2, delta_frames):
472519
return match_event_counts_df
473520

474521

475-
def make_agreement_scores(sorting1, sorting2, delta_frames):
522+
def make_agreement_scores(sorting1, sorting2, delta_frames, ensure_symmetry=True):
476523
"""
477524
Make the agreement matrix.
478525
No threshold (min_score) is applied at this step.
479526
480-
Note : this computation is symmetric.
527+
Note : this computation is symmetric by default.
481528
Inverting sorting1 and sorting2 give the transposed matrix.
482529
483530
Parameters
@@ -488,7 +535,9 @@ def make_agreement_scores(sorting1, sorting2, delta_frames):
488535
The second sorting extractor
489536
delta_frames: int
490537
Number of frames to consider spikes coincident
491-
538+
ensure_symmetry: bool, default: True
539+
If ensure_symmetry is True, then the algo is run two times by switching sorting1 and sorting2.
540+
And the minimum of the two results is taken.
492541
Returns
493542
-------
494543
agreement_scores: array (float)
@@ -504,7 +553,7 @@ def make_agreement_scores(sorting1, sorting2, delta_frames):
504553
event_counts1 = pd.Series(ev_counts1, index=unit1_ids)
505554
event_counts2 = pd.Series(ev_counts2, index=unit2_ids)
506555

507-
match_event_count = make_match_count_matrix(sorting1, sorting2, delta_frames)
556+
match_event_count = make_match_count_matrix(sorting1, sorting2, delta_frames, ensure_symmetry=ensure_symmetry)
508557

509558
agreement_scores = make_agreement_scores_from_count(match_event_count, event_counts1, event_counts2)
510559

src/spikeinterface/comparison/paircomparisons.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(
2828
delta_time=0.4,
2929
match_score=0.5,
3030
chance_score=0.1,
31+
ensure_symmetry=False,
3132
n_jobs=1,
3233
verbose=False,
3334
):
@@ -55,6 +56,8 @@ def __init__(
5556
self.unit1_ids = self.sorting1.get_unit_ids()
5657
self.unit2_ids = self.sorting2.get_unit_ids()
5758

59+
self.ensure_symmetry = ensure_symmetry
60+
5861
self._do_agreement()
5962
self._do_matching()
6063

@@ -84,7 +87,9 @@ def _do_agreement(self):
8487
self.event_counts2 = do_count_event(self.sorting2)
8588

8689
# matrix of event match count for each pair
87-
self.match_event_count = make_match_count_matrix(self.sorting1, self.sorting2, self.delta_frames)
90+
self.match_event_count = make_match_count_matrix(
91+
self.sorting1, self.sorting2, self.delta_frames, ensure_symmetry=self.ensure_symmetry
92+
)
8893

8994
# agreement matrix score for each pair
9095
self.agreement_scores = make_agreement_scores_from_count(
@@ -151,6 +156,7 @@ def __init__(
151156
delta_time=delta_time,
152157
match_score=match_score,
153158
chance_score=chance_score,
159+
ensure_symmetry=True,
154160
n_jobs=n_jobs,
155161
verbose=verbose,
156162
)
@@ -283,6 +289,7 @@ def __init__(
283289
delta_time=delta_time,
284290
match_score=match_score,
285291
chance_score=chance_score,
292+
ensure_symmetry=False,
286293
n_jobs=n_jobs,
287294
verbose=verbose,
288295
)

0 commit comments

Comments
 (0)