Skip to content

Commit 36c723f

Browse files
authored
Merge branch 'SpikeInterface:main' into fix_silence_periods
2 parents db2b4d5 + 0963611 commit 36c723f

16 files changed

Lines changed: 239 additions & 154 deletions

File tree

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,20 @@
11
name: Install packages
22
description: This action installs the package and its dependencies for testing
33

4-
inputs:
5-
python-version:
6-
description: 'Python version to set up'
7-
required: false
8-
os:
9-
description: 'Operating system to set up'
10-
required: false
11-
124
runs:
135
using: "composite"
146
steps:
157
- name: Install dependencies
168
run: |
17-
sudo apt install git
189
git config --global user.email "CI@example.com"
1910
git config --global user.name "CI Almighty"
20-
python -m venv ${{ github.workspace }}/test_env # Environment used in the caching step
21-
python -m pip install -U pip # Official recommended way
22-
source ${{ github.workspace }}/test_env/bin/activate
2311
pip install tabulate # This produces summaries at the end
2412
pip install -e .[test,extractors,streaming_extractors,test_extractors,full]
2513
shell: bash
26-
- name: Force installation of latest dev from key-packages when running dev (not release)
27-
run: |
28-
source ${{ github.workspace }}/test_env/bin/activate
29-
spikeinterface_is_dev_version=$(python -c "import spikeinterface; print(spikeinterface.DEV_MODE)")
30-
if [ $spikeinterface_is_dev_version = "True" ]; then
31-
echo "Running spikeinterface dev version"
32-
pip install --no-cache-dir git+https://github.com/NeuralEnsemble/python-neo
33-
pip install --no-cache-dir git+https://github.com/SpikeInterface/probeinterface
34-
fi
35-
echo "Running tests for release, using pyproject.toml versions of neo and probeinterface"
14+
- name: Install git-annex
3615
shell: bash
37-
- name: git-annex install
3816
run: |
17+
pip install datalad-installer
3918
wget https://downloads.kitenet.net/git-annex/linux/current/git-annex-standalone-amd64.tar.gz
4019
mkdir /home/runner/work/installation
4120
mv git-annex-standalone-amd64.tar.gz /home/runner/work/installation/
@@ -44,4 +23,14 @@ runs:
4423
tar xvzf git-annex-standalone-amd64.tar.gz
4524
echo "$(pwd)/git-annex.linux" >> $GITHUB_PATH
4625
cd $workdir
26+
git config --global filter.annex.process "git-annex filter-process" # recommended for efficiency
27+
- name: Force installation of latest dev from key-packages when running dev (not release)
28+
run: |
29+
spikeinterface_is_dev_version=$(python -c "import spikeinterface; print(spikeinterface.DEV_MODE)")
30+
if [ $spikeinterface_is_dev_version = "True" ]; then
31+
echo "Running spikeinterface dev version"
32+
pip install --no-cache-dir git+https://github.com/NeuralEnsemble/python-neo
33+
pip install --no-cache-dir git+https://github.com/SpikeInterface/probeinterface
34+
fi
35+
echo "Running tests for release, using pyproject.toml versions of neo and probeinterface"
4736
shell: bash

.github/workflows/all-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
echo "$file was changed"
4848
done
4949
50-
- name: Set testing environment # This decides which tests are run and whether to install especial dependencies
50+
- name: Set testing environment # This decides which tests are run and whether to install special dependencies
5151
shell: bash
5252
run: |
5353
changed_files="${{ steps.changed-files.outputs.all_changed_files }}"

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ jobs:
4545
env:
4646
HDF5_PLUGIN_PATH: ${{ github.workspace }}/hdf5_plugin_path_maxwell
4747
run: |
48-
source ${{ github.workspace }}/test_env/bin/activate
4948
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
5049
echo "# Timing profile of full tests" >> $GITHUB_STEP_SUMMARY
5150
python ./.github/scripts/build_job_summary.py report_full.txt >> $GITHUB_STEP_SUMMARY

doc/get_started/quickstart.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,7 @@ compute quality metrics (some quality metrics require certain extensions
673673
'min_spikes': 0,
674674
'window_size_s': 1},
675675
'snr': {'peak_mode': 'extremum', 'peak_sign': 'neg'},
676-
'synchrony': {'synchrony_sizes': (2, 4, 8)}}
676+
'synchrony': {}
677677
678678
679679
Since the recording is very short, let’s change some parameters to

doc/modules/qualitymetrics/synchrony.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ trains. This way synchronous events can be found both in multi-unit and single-u
1212
Complexity is calculated by counting the number of spikes (i.e. non-empty bins) that occur at the same sample index,
1313
within and across spike trains.
1414

15-
Synchrony metrics can be computed for different synchrony sizes (>1), defining the number of simultaneous spikes to count.
15+
Synchrony metrics are computed for 2, 4 and 8 synchronous spikes.
1616

1717

1818

@@ -29,7 +29,7 @@ Example code
2929
3030
import spikeinterface.qualitymetrics as sqm
3131
# Combine a sorting and recording into a sorting_analyzer
32-
synchrony = sqm.compute_synchrony_metrics(sorting_analyzer=sorting_analyzer synchrony_sizes=(2, 4, 8))
32+
synchrony = sqm.compute_synchrony_metrics(sorting_analyzer=sorting_analyzer)
3333
# synchrony is a tuple of dicts with the synchrony metrics for each unit
3434
3535

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ extractors = [
7373
]
7474

7575
streaming_extractors = [
76-
"ONE-api>=2.7.0", # alf sorter and streaming IBL
76+
"ONE-api>=2.7.0,<2.10.0", # alf sorter and streaming IBL
7777
"ibllib>=2.36.0", # streaming IBL
7878
# Following dependencies are for streaming with nwb files
7979
"pynwb>=2.6.0",

src/spikeinterface/postprocessing/template_metrics.py

Lines changed: 77 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,10 @@ class ComputeTemplateMetrics(AnalyzerExtension):
6363
include_multi_channel_metrics : bool, default: False
6464
Whether to compute multi-channel metrics
6565
delete_existing_metrics : bool, default: False
66-
If True, any template metrics attached to the `sorting_analyzer` are deleted. If False, any metrics which were previously calculated but are not included in `metric_names` are kept, provided the `metrics_kwargs` are unchanged.
67-
metrics_kwargs : dict
68-
Additional arguments to pass to the metric functions. Including:
69-
* recovery_window_ms: the window in ms after the peak to compute the recovery_slope, default: 0.7
70-
* peak_relative_threshold: the relative threshold to detect positive and negative peaks, default: 0.2
71-
* peak_width_ms: the width in samples to detect peaks, default: 0.2
72-
* depth_direction: the direction to compute velocity above and below, default: "y" (see notes)
73-
* min_channels_for_velocity: the minimum number of channels above or below to compute velocity, default: 5
74-
* min_r2_velocity: the minimum r2 to accept the velocity fit, default: 0.7
75-
* exp_peak_function: the function to use to compute the peak amplitude for the exp decay, default: "ptp"
76-
* min_r2_exp_decay: the minimum r2 to accept the exp decay fit, default: 0.5
77-
* spread_threshold: the threshold to compute the spread, default: 0.2
78-
* spread_smooth_um: the smoothing in um to compute the spread, default: 20
79-
* column_range: the range in um in the horizontal direction to consider channels for velocity, default: None
80-
- If None, all channels all channels are considered
81-
- If 0 or 1, only the "column" that includes the max channel is considered
82-
- If > 1, only channels within range (+/-) um from the max channel horizontal position are used
66+
If True, any template metrics attached to the `sorting_analyzer` are deleted. If False, any metrics which were previously calculated but are not included in `metric_names` are kept, provided the `metric_params` are unchanged.
67+
metric_params : dict of dicts or None, default: None
68+
Dictionary with parameters for template metrics calculation.
69+
Default parameters can be obtained with: `si.postprocessing.template_metrics.get_default_tm_params()`
8370
8471
Returns
8572
-------
@@ -100,15 +87,29 @@ class ComputeTemplateMetrics(AnalyzerExtension):
10087
need_recording = False
10188
use_nodepipeline = False
10289
need_job_kwargs = False
90+
need_backward_compatibility_on_load = True
10391

10492
min_channels_for_multi_channel_warning = 10
10593

94+
def _handle_backward_compatibility_on_load(self):
95+
96+
# For backwards compatibility - this reformats metrics_kwargs as metric_params
97+
if (metrics_kwargs := self.params.get("metrics_kwargs")) is not None:
98+
99+
metric_params = {}
100+
for metric_name in self.params["metric_names"]:
101+
metric_params[metric_name] = deepcopy(metrics_kwargs)
102+
self.params["metric_params"] = metric_params
103+
104+
del self.params["metrics_kwargs"]
105+
106106
def _set_params(
107107
self,
108108
metric_names=None,
109109
peak_sign="neg",
110110
upsampling_factor=10,
111111
sparsity=None,
112+
metric_params=None,
112113
metrics_kwargs=None,
113114
include_multi_channel_metrics=False,
114115
delete_existing_metrics=False,
@@ -134,33 +135,24 @@ def _set_params(
134135
if include_multi_channel_metrics:
135136
metric_names += get_multi_channel_template_metric_names()
136137

137-
if metrics_kwargs is None:
138-
metrics_kwargs_ = _default_function_kwargs.copy()
139-
if len(other_kwargs) > 0:
140-
for m in other_kwargs:
141-
if m in metrics_kwargs_:
142-
metrics_kwargs_[m] = other_kwargs[m]
143-
else:
144-
metrics_kwargs_ = _default_function_kwargs.copy()
145-
metrics_kwargs_.update(metrics_kwargs)
138+
if metrics_kwargs is not None and metric_params is None:
139+
deprecation_msg = "`metrics_kwargs` is deprecated and will be removed in version 0.104.0. Please use `metric_params` instead"
140+
deprecation_msg = "`metrics_kwargs` is deprecated and will be removed in version 0.104.0. Please use `metric_params` instead"
141+
142+
metric_params = {}
143+
for metric_name in metric_names:
144+
metric_params[metric_name] = deepcopy(metrics_kwargs)
145+
146+
metric_params_ = get_default_tm_params(metric_names)
147+
for k in metric_params_:
148+
if metric_params is not None and k in metric_params:
149+
metric_params_[k].update(metric_params[k])
146150

147151
metrics_to_compute = metric_names
148152
tm_extension = self.sorting_analyzer.get_extension("template_metrics")
149153
if delete_existing_metrics is False and tm_extension is not None:
150154

151-
existing_params = tm_extension.params["metrics_kwargs"]
152-
# checks that existing metrics were calculated using the same params
153-
if existing_params != metrics_kwargs_:
154-
warnings.warn(
155-
f"The parameters used to calculate the previous template metrics are different"
156-
f"than those used now.\nPrevious parameters: {existing_params}\nCurrent "
157-
f"parameters: {metrics_kwargs_}\nDeleting previous template metrics..."
158-
)
159-
tm_extension.params["metric_names"] = []
160-
existing_metric_names = []
161-
else:
162-
existing_metric_names = tm_extension.params["metric_names"]
163-
155+
existing_metric_names = tm_extension.params["metric_names"]
164156
existing_metric_names_propogated = [
165157
metric_name for metric_name in existing_metric_names if metric_name not in metrics_to_compute
166158
]
@@ -171,7 +163,7 @@ def _set_params(
171163
sparsity=sparsity,
172164
peak_sign=peak_sign,
173165
upsampling_factor=int(upsampling_factor),
174-
metrics_kwargs=metrics_kwargs_,
166+
metric_params=metric_params_,
175167
delete_existing_metrics=delete_existing_metrics,
176168
metrics_to_compute=metrics_to_compute,
177169
)
@@ -273,7 +265,7 @@ def _compute_metrics(self, sorting_analyzer, unit_ids=None, verbose=False, metri
273265
sampling_frequency=sampling_frequency_up,
274266
trough_idx=trough_idx,
275267
peak_idx=peak_idx,
276-
**self.params["metrics_kwargs"],
268+
**self.params["metric_params"][metric_name],
277269
)
278270
except Exception as e:
279271
warnings.warn(f"Error computing metric {metric_name} for unit {unit_id}: {e}")
@@ -312,7 +304,7 @@ def _compute_metrics(self, sorting_analyzer, unit_ids=None, verbose=False, metri
312304
template_upsampled,
313305
channel_locations=channel_locations_sparse,
314306
sampling_frequency=sampling_frequency_up,
315-
**self.params["metrics_kwargs"],
307+
**self.params["metric_params"][metric_name],
316308
)
317309
except Exception as e:
318310
warnings.warn(f"Error computing metric {metric_name} for unit {unit_id}: {e}")
@@ -326,8 +318,8 @@ def _compute_metrics(self, sorting_analyzer, unit_ids=None, verbose=False, metri
326318

327319
def _run(self, verbose=False):
328320

329-
delete_existing_metrics = self.params["delete_existing_metrics"]
330321
metrics_to_compute = self.params["metrics_to_compute"]
322+
delete_existing_metrics = self.params["delete_existing_metrics"]
331323

332324
# compute the metrics which have been specified by the user
333325
computed_metrics = self._compute_metrics(
@@ -343,9 +335,21 @@ def _run(self, verbose=False):
343335
):
344336
existing_metrics = tm_extension.params["metric_names"]
345337

338+
existing_metrics = []
339+
# here we get in the loaded via the dict only (to avoid full loading from disk after params reset)
340+
tm_extension = self.sorting_analyzer.extensions.get("template_metrics", None)
341+
if (
342+
delete_existing_metrics is False
343+
and tm_extension is not None
344+
and tm_extension.data.get("metrics") is not None
345+
):
346+
existing_metrics = tm_extension.params["metric_names"]
347+
346348
# append the metrics which were previously computed
347349
for metric_name in set(existing_metrics).difference(metrics_to_compute):
348-
computed_metrics[metric_name] = tm_extension.data["metrics"][metric_name]
350+
# some metrics names produce data columns with other names. This deals with that.
351+
for column_name in tm_compute_name_to_column_names[metric_name]:
352+
computed_metrics[column_name] = tm_extension.data["metrics"][column_name]
349353

350354
self.data["metrics"] = computed_metrics
351355

@@ -372,6 +376,35 @@ def _get_data(self):
372376
)
373377

374378

379+
def get_default_tm_params(metric_names):
380+
if metric_names is None:
381+
metric_names = get_template_metric_names()
382+
383+
base_tm_params = _default_function_kwargs
384+
385+
metric_params = {}
386+
for metric_name in metric_names:
387+
metric_params[metric_name] = deepcopy(base_tm_params)
388+
389+
return metric_params
390+
391+
392+
# a dict converting the name of the metric for computation to the output of that computation
393+
tm_compute_name_to_column_names = {
394+
"peak_to_valley": ["peak_to_valley"],
395+
"peak_trough_ratio": ["peak_trough_ratio"],
396+
"half_width": ["half_width"],
397+
"repolarization_slope": ["repolarization_slope"],
398+
"recovery_slope": ["recovery_slope"],
399+
"num_positive_peaks": ["num_positive_peaks"],
400+
"num_negative_peaks": ["num_negative_peaks"],
401+
"velocity_above": ["velocity_above"],
402+
"velocity_below": ["velocity_below"],
403+
"exp_decay": ["exp_decay"],
404+
"spread": ["spread"],
405+
}
406+
407+
375408
def get_trough_and_peak_idx(template):
376409
"""
377410
Return the indices into the input template of the detected trough

src/spikeinterface/postprocessing/tests/test_template_metrics.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from spikeinterface.postprocessing.tests.common_extension_tests import AnalyzerExtensionCommonTestSuite
2-
from spikeinterface.postprocessing import ComputeTemplateMetrics
2+
from spikeinterface.postprocessing import ComputeTemplateMetrics, compute_template_metrics
33
import pytest
44
import csv
55

@@ -8,6 +8,49 @@
88
template_metrics = list(_single_channel_metric_name_to_func.keys())
99

1010

11+
def test_different_params_template_metrics(small_sorting_analyzer):
12+
"""
13+
Computes template metrics using different params, and check that they are
14+
actually calculated using the different params.
15+
"""
16+
compute_template_metrics(
17+
sorting_analyzer=small_sorting_analyzer,
18+
metric_names=["exp_decay", "spread", "half_width"],
19+
metric_params={"exp_decay": {"recovery_window_ms": 0.8}, "spread": {"spread_smooth_um": 15}},
20+
)
21+
22+
tm_extension = small_sorting_analyzer.get_extension("template_metrics")
23+
tm_params = tm_extension.params["metric_params"]
24+
25+
assert tm_params["exp_decay"]["recovery_window_ms"] == 0.8
26+
assert tm_params["spread"]["recovery_window_ms"] == 0.7
27+
assert tm_params["half_width"]["recovery_window_ms"] == 0.7
28+
29+
assert tm_params["spread"]["spread_smooth_um"] == 15
30+
assert tm_params["exp_decay"]["spread_smooth_um"] == 20
31+
assert tm_params["half_width"]["spread_smooth_um"] == 20
32+
33+
34+
def test_backwards_compat_params_template_metrics(small_sorting_analyzer):
35+
"""
36+
Computes template metrics using the metrics_kwargs keyword
37+
"""
38+
compute_template_metrics(
39+
sorting_analyzer=small_sorting_analyzer,
40+
metric_names=["exp_decay", "spread"],
41+
metrics_kwargs={"recovery_window_ms": 0.8},
42+
)
43+
44+
tm_extension = small_sorting_analyzer.get_extension("template_metrics")
45+
tm_params = tm_extension.params["metric_params"]
46+
47+
assert tm_params["exp_decay"]["recovery_window_ms"] == 0.8
48+
assert tm_params["spread"]["recovery_window_ms"] == 0.8
49+
50+
assert tm_params["spread"]["spread_smooth_um"] == 20
51+
assert tm_params["exp_decay"]["spread_smooth_um"] == 20
52+
53+
1154
def test_compute_new_template_metrics(small_sorting_analyzer):
1255
"""
1356
Computes template metrics then computes a subset of template metrics, and checks
@@ -17,6 +60,8 @@ def test_compute_new_template_metrics(small_sorting_analyzer):
1760
are deleted.
1861
"""
1962

63+
small_sorting_analyzer.delete_extension("template_metrics")
64+
2065
# calculate just exp_decay
2166
small_sorting_analyzer.compute({"template_metrics": {"metric_names": ["exp_decay"]}})
2267
template_metric_extension = small_sorting_analyzer.get_extension("template_metrics")
@@ -47,7 +92,7 @@ def test_compute_new_template_metrics(small_sorting_analyzer):
4792

4893
# check that, when parameters are changed, the old metrics are deleted
4994
small_sorting_analyzer.compute(
50-
{"template_metrics": {"metric_names": ["exp_decay"], "metrics_kwargs": {"recovery_window_ms": 0.6}}}
95+
{"template_metrics": {"metric_names": ["exp_decay"], "metric_params": {"recovery_window_ms": 0.6}}}
5196
)
5297

5398

src/spikeinterface/postprocessing/unit_locations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class ComputeUnitLocations(AnalyzerExtension):
2626
----------
2727
sorting_analyzer : SortingAnalyzer
2828
A SortingAnalyzer object
29-
method : "center_of_mass" | "monopolar_triangulation" | "grid_convolution", default: "center_of_mass"
29+
method : "monopolar_triangulation" | "center_of_mass" | "grid_convolution", default: "monopolar_triangulation"
3030
The method to use for localization
3131
**method_kwargs : dict, default: {}
3232
Kwargs which are passed to the method function. These can be found in the docstrings of `compute_center_of_mass`, `compute_grid_convolution` and `compute_monopolar_triangulation`.

0 commit comments

Comments
 (0)