Skip to content

Commit 3edabe0

Browse files
authored
Change exclude noise (#13)
* Update and update tests. * Tidy up old noise references. * Fix tests. * Fix CI. * Add Qt deps to linux CI.
1 parent 6f5e1fc commit 3edabe0

18 files changed

Lines changed: 226 additions & 142 deletions

.github/workflows/test_and_deploy.yml

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,46 @@ jobs:
3737
python-version: "3.13"
3838

3939
steps:
40-
# Run tests
41-
- uses: neuroinformatics-unit/actions/test@v2
40+
- uses: actions/checkout@v4
41+
42+
- uses: actions/setup-python@v5
4243
with:
4344
python-version: ${{ matrix.python-version }}
4445

46+
- name: Install Qt system libraries (Linux)
47+
if: runner.os == 'Linux'
48+
run: |
49+
sudo apt-get update
50+
sudo apt-get install -y --no-install-recommends \
51+
libegl1 \
52+
libgl1 \
53+
libxkbcommon-x11-0 \
54+
libdbus-1-3 \
55+
libxcb-cursor0 \
56+
libxcb-icccm4 \
57+
libxcb-image0 \
58+
libxcb-keysyms1 \
59+
libxcb-randr0 \
60+
libxcb-render-util0 \
61+
libxcb-shape0 \
62+
libxcb-sync1 \
63+
libxcb-xfixes0 \
64+
libxcb-xinerama0 \
65+
libxcb-xkb1 \
66+
libxkbcommon0 \
67+
xvfb
68+
69+
- name: Install package
70+
run: python -m pip install -e ".[dev]"
71+
72+
- name: Run tests (Linux, headless)
73+
if: runner.os == 'Linux'
74+
run: xvfb-run -a python -m pytest .
75+
76+
- name: Run tests
77+
if: runner.os != 'Linux'
78+
run: python -m pytest .
79+
4580
build_sdist_wheels:
4681
name: Build source distribution
4782
needs: [test]

docs/source/pages/how-to-use-driftplots.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ unit that spike is assigned to.
77
Below we will cover the main ways to use `driftplots`.
88
See the [API Reference](/pages/api_index) for a full list of arguments.
99

10-
:::{note}
1110
See [here](terminology) for a glossary of key terms.
11+
12+
::: {warning}
13+
`driftplots` was designed and tested with Neuropixels probes, but it should also work with most other probes.
14+
Please raise a [GitHub issue](https://github.com/neuroinformatics-unit/driftplots) if you have any problems.
1215
:::
1316

17+
1418
## Inputs
1519

1620
`driftplots` accepts either a path to the output of Kilosort, or a SpikeInterface `SortingAnalyzer` as input.
@@ -29,15 +33,13 @@ on the argument set for `max_spikes_per_unit` used when computing `"random_spike
2933

3034
By default, the number of spikes displayed will be decimated to around `100,000`.
3135

32-
::: {warning}
33-
`driftplots` was designed and tested with Neuropixels probes, but it should also work with most other probes.
34-
Please raise a [GitHub issue](https://github.com/neuroinformatics-unit/driftplots) if you have any problems.
36+
::: {tip}
37+
`good_units_only=True` is a useful way of excluding spikes from noise and MUA units, tidying up the drift map.
3538
:::
3639

37-
3840
## Interactive Viewer
3941

40-
{py:meth}`~driftplots.DriftPlotter.drift_map_plot_matplotlib` generates an interactive viewer
42+
{py:meth}`~driftplots.DriftPlotter.drift_map_plot_interactive` generates an interactive viewer
4143
allowing the selection of individual spikes on the driftmap. Once selected, the template for the
4244
unit that spike is associated with will be displayed on the right-hand side.
4345

@@ -54,7 +56,7 @@ plotter = DriftPlotter(
5456
)
5557

5658
driftmap = plotter.drift_map_plot_interactive(
57-
exclude_noise=True,
59+
good_units_only=True,
5860
)
5961

6062
driftmap.plot()
@@ -160,7 +162,6 @@ across all sessions and applied to all plots.
160162
```python
161163
import numpy as np
162164
import spikeinterface as si
163-
from PySide6 import QtWidgets[how-parameters-are-calculated.md](how-parameters-are-calculated.md)
164165

165166
from driftplots import DriftPlotter, MultiSessionDriftmapWidget, get_amplitudes
166167

@@ -172,7 +173,7 @@ SORTING_SESSIONS = [
172173
]
173174

174175
all_spike_amplitudes = get_amplitudes(
175-
SORTING_SESSIONS, exclude_noise=False, concatenate=True
176+
SORTING_SESSIONS, good_units_only=True, concatenate=True
176177
)
177178

178179
min_cutoff, max_cutoff = np.percentile(all_spike_amplitudes, (0, 95))

driftplots/amplitudes.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
def get_amplitudes(
1212
list_of_path_or_analyzer: list[Path | si.SortingAnalyzer],
13-
exclude_noise: bool = False,
13+
good_units_only: bool | str = False,
1414
concatenate: bool = False,
1515
verbose: bool = True,
1616
) -> np.ndarray | list[np.ndarray]:
@@ -21,9 +21,13 @@ def get_amplitudes(
2121
list_of_path_or_analyzer
2222
Kilosort output directory paths or SpikeInterface ``SortingAnalyzer``
2323
objects to load amplitudes from. Can be a mix of both.
24-
exclude_noise
25-
If ``True``, remove spikes belonging to clusters labelled "noise"
26-
before returning amplitudes.
24+
good_units_only
25+
If ``True``, only spikes belonging to "good" units are displayed.
26+
For Kilosort, this is taken from the
27+
cluster_groups.csv / cluster_group.tsv file that reflects labels
28+
set in Phy. For a SortingAnalyzer, a string must be passed.
29+
The labels are taken from the sorting property with the
30+
passed name (e.g. "KSLabel").
2731
concatenate
2832
If ``True``, concatenate all per-session amplitude arrays into a
2933
single 1-D array. If ``False`` (default), return a list with one
@@ -44,7 +48,7 @@ def get_amplitudes(
4448
loader = DataLoader(path_or_analyzer, verbose)
4549

4650
processed_data = loader.get_processed_data(
47-
exclude_noise,
51+
good_units_only,
4852
decimate=False,
4953
filter_amplitude_mode=None,
5054
filter_amplitude_values=None,

driftplots/data_loader.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,23 +69,18 @@ def __init__(
6969

7070
def get_processed_data(
7171
self,
72-
exclude_noise,
72+
good_units_only,
7373
decimate,
7474
filter_amplitude_mode,
7575
filter_amplitude_values,
7676
verbose: bool,
7777
):
7878
"""Filter and subsample the loaded spike data.
7979
80-
Operations are applied in order: decimation → noise exclusion →
81-
amplitude filtering → masking. Decimation is applied first as a
82-
performance knob to thin the full dataset before further filtering.
83-
8480
Parameters
8581
----------
86-
exclude_noise :
87-
If ``True``, spikes belonging to clusters labelled "noise" in
88-
the Kilosort cluster groups file are removed.
82+
good_units_only
83+
If ``True``, only spikes belonging to "good" units are kept.
8984
decimate :
9085
Keep every *n*-th spike. Applied first to reduce the dataset
9186
before noise/amplitude filters. ``False`` disables decimation.
@@ -115,19 +110,19 @@ def get_processed_data(
115110

116111
keep_bool_mask = None
117112

118-
# First, exclude spikes from units labeled as "noise"
119-
if exclude_noise:
113+
# First, exclude spikes from units that are not labelled "good"
114+
if good_units_only:
120115
if isinstance(self.path_or_analyzer, si.SortingAnalyzer):
121-
keep_bool_mask = ~analyzer_helpers.get_noise_mask(
122-
exclude_noise, spike_templates, self.path_or_analyzer
116+
keep_bool_mask = analyzer_helpers.get_good_unit_mask(
117+
good_units_only, spike_templates, self.path_or_analyzer
123118
)
124119
else:
125-
keep_bool_mask = ~kilosort_helpers.get_noise_mask(
120+
keep_bool_mask = kilosort_helpers.get_good_unit_mask(
126121
spike_templates, self.path_or_analyzer
127122
)
128123

129124
self._print(
130-
f"Excluding noise spikes. {keep_bool_mask.sum()} spikes remaining.",
125+
f"Keeping good spikes only. {keep_bool_mask.sum()} spikes remaining.",
131126
verbose,
132127
)
133128

@@ -154,7 +149,7 @@ def get_processed_data(
154149
verbose,
155150
)
156151

157-
# mask exclude_noise / filtered amplitudes
152+
# mask good_units_only / filtered amplitudes
158153
if keep_bool_mask is not None:
159154
spike_times = spike_times[keep_bool_mask]
160155
spike_amplitudes = spike_amplitudes[keep_bool_mask]

driftplots/driftplotter.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@
1111
from driftplots.data_loader import DataLoader
1212
from driftplots.interactive.driftmap_plot_widget import DriftmapPlotWidget
1313

14-
# test ideas:
15-
# check signatures match default args between interactive and matplotlib
16-
1714

1815
class DriftPlotter:
1916
"""Load Kilosort or SpikeInterface output and plot drift maps.
@@ -46,7 +43,7 @@ def __init__(self, path_or_analyzer: str | Path, verbose: bool = True) -> None:
4643
def drift_map_plot_interactive(
4744
self,
4845
decimate: int | bool | None | Literal["estimate"] = "estimate",
49-
exclude_noise: bool | str = False,
46+
good_units_only: bool | str = False,
5047
amplitude_cmap_scaling: str | tuple[float, float] = "linear",
5148
n_color_bins: int = 20,
5249
point_size: float = 5.0,
@@ -64,12 +61,13 @@ def drift_map_plot_interactive(
6461
automatically reduce spikes to a reasonable count (≈ 100 000).
6562
Pass ``False``, ``None``, or ``0`` to disable decimation. Pass
6663
an integer *n* to keep every *n*-th spike.
67-
exclude_noise
68-
If ``True``, remove all spikes belonging to clusters labelled
69-
"noise". For Kilosort, this is taken from the cluster_groups.csv /
70-
cluster_group.tsv file that reflects labels set in Phy. For a
71-
SortingAnalyzer, a string must be passed. The labels are taken from
72-
the sorting property with the passed name (e.g. "KSLabel").
64+
good_units_only
65+
If ``True``, only spikes belonging to "good" units are displayed.
66+
For Kilosort, this is taken from the
67+
cluster_groups.csv / cluster_group.tsv file that reflects labels
68+
set in Phy. For a SortingAnalyzer, a string must be passed.
69+
The labels are taken from the sorting property with the
70+
passed name (e.g. "KSLabel").
7371
amplitude_cmap_scaling
7472
Controls how spike amplitudes are mapped to the greyscale
7573
colormap. Pass ``"linear"`` or ``"log2"`` or ``"log10"`` for automatic
@@ -105,7 +103,7 @@ def drift_map_plot_interactive(
105103
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
106104

107105
processed_data = self._data_loader.get_processed_data(
108-
exclude_noise,
106+
good_units_only,
109107
decimate,
110108
filter_amplitude_mode,
111109
filter_amplitude_values,
@@ -126,7 +124,7 @@ def drift_map_plot_interactive(
126124
def drift_map_plot_matplotlib(
127125
self,
128126
decimate: int | bool | None | Literal["estimate"] = "estimate",
129-
exclude_noise: bool | str = False,
127+
good_units_only: bool | str = False,
130128
amplitude_cmap_scaling: str | tuple[float, float] = "linear",
131129
n_color_bins: int = 20,
132130
point_size: float = 5.0,
@@ -147,12 +145,13 @@ def drift_map_plot_matplotlib(
147145
automatically reduce spikes to a reasonable count (≈ 100 000).
148146
Pass ``False``, ``None``, or ``0`` to disable decimation. Pass
149147
an integer *n* to keep every *n*-th spike.
150-
exclude_noise
151-
If ``True``, remove all spikes belonging to clusters labelled
152-
"noise". For Kilosort, this is taken from the cluster_groups.csv /
153-
cluster_group.tsv file that reflects labels set in Phy. For a
154-
SortingAnalyzer, a string must be passed. The labels are taken from
155-
the sorting property with the passed name (e.g. "KSLabel").
148+
good_units_only
149+
If ``True``, only spikes belonging to "good" units are displayed.
150+
For Kilosort, this is taken from the
151+
cluster_groups.csv / cluster_group.tsv file that reflects labels
152+
set in Phy. For a SortingAnalyzer, a string must be passed.
153+
The labels are taken from the sorting property with the
154+
passed name (e.g. "KSLabel").
156155
amplitude_cmap_scaling
157156
Controls how spike amplitudes are mapped to the greyscale
158157
colormap. Pass ``"linear"`` or ``"log2"`` or ``"log10"`` for automatic
@@ -196,7 +195,7 @@ def drift_map_plot_matplotlib(
196195
The populated Matplotlib figure.
197196
"""
198197
processed_data = self._data_loader.get_processed_data(
199-
exclude_noise,
198+
good_units_only,
200199
decimate,
201200
filter_amplitude_mode,
202201
filter_amplitude_values,

driftplots/extractors/analyzer_helpers.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,27 +56,28 @@ def get_sorting_analyzer(
5656
)
5757

5858

59-
def get_noise_mask(
60-
exclude_noise: bool | str,
59+
def get_good_unit_mask(
60+
good_units_only: bool | str,
6161
spike_templates: np.ndarray,
6262
analyzer: si.SortingAnalyzer,
6363
) -> np.ndarray:
6464
""" """
65-
if exclude_noise is True:
65+
if good_units_only is True:
6666
raise ValueError(
67-
f"When using SortingAnalyzer, `exclude_noise` must be a string of the "
68-
f"name of the labels to use, passed to `analyzer.get_sorting_property()."
67+
f"When using SortingAnalyzer, `good_units_only` must be a string of the "
68+
f"name of the sorting property to use."
6969
f"Properties on this analyzer are: {analyzer.sorting.get_property_keys()}"
7070
)
7171

72-
assert isinstance(exclude_noise, str), "`exclude_noise` must be a string"
73-
labels = analyzer.get_sorting_property(exclude_noise)
72+
assert isinstance(good_units_only, str), "`good_units_only` must be a string"
73+
labels = analyzer.get_sorting_property(good_units_only)
7474

7575
if labels is None:
7676
raise ValueError(
77-
f"The analyzer does not contain a sorting property called: {exclude_noise}"
77+
"The analyzer does not contain a sorting property called: "
78+
f"{good_units_only}"
7879
)
7980

80-
noise_mask = (labels == "noise")[spike_templates]
81+
good_mask = (labels == "good")[spike_templates]
8182

82-
return noise_mask
83+
return good_mask

driftplots/extractors/kilosort_helpers.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ def load_cluster_groups(cluster_path: Path) -> tuple[np.ndarray, np.ndarray]:
5050
return cluster_ids, cluster_groups
5151

5252

53-
def get_noise_mask(spike_templates: np.ndarray, sorter_output: Path) -> np.ndarray:
54-
"""Build a boolean mask identifying spikes that belong to noise-labelled templates.
53+
def get_good_unit_mask(spike_templates: np.ndarray, sorter_output: Path) -> np.ndarray:
54+
"""Build a boolean mask identifying spikes that belong to good-labelled templates.
5555
5656
Loads the cluster-groups file (``cluster_groups.csv`` or
5757
``cluster_group.tsv``) from the sorter output directory. Spikes
58-
whose cluster is labelled *noise* (group == 0) are marked ``True``.
58+
whose cluster is labelled *good* (group == 2) are marked ``True``.
5959
6060
Parameters
6161
----------
@@ -68,30 +68,24 @@ def get_noise_mask(spike_templates: np.ndarray, sorter_output: Path) -> np.ndarr
6868
-------
6969
np.ndarray
7070
(num_spikes,) boolean array — ``True`` for spikes belonging to
71-
a noise-labelled template.
72-
73-
Raises
74-
------
75-
ValueError
76-
If neither ``cluster_groups.csv`` nor ``cluster_group.tsv``
77-
exists in ``sorter_output``.
71+
a good-labelled template.
7872
"""
7973
if not (
8074
(cluster_path := sorter_output / "cluster_groups.csv").is_file()
8175
or (cluster_path := sorter_output / "cluster_group.tsv").is_file()
8276
):
8377
raise ValueError(
84-
f"`exclude_noise` is `True` but there is no `cluster_groups.csv/.tsv` "
78+
f"`good_units_only` is `True` but there is no `cluster_groups.csv/.tsv` "
8579
f"in the sorting output at: {sorter_output}"
8680
)
8781

8882
cluster_ids, cluster_groups = load_cluster_groups(cluster_path)
8983

90-
noise_cluster_ids = cluster_ids[cluster_groups == 0]
84+
good_cluster_ids = cluster_ids[cluster_groups == 2]
9185

92-
exclude_bool_mask = np.isin(spike_templates.ravel(), noise_cluster_ids)
86+
good_mask = np.isin(spike_templates.ravel(), good_cluster_ids)
9387

94-
return exclude_bool_mask
88+
return good_mask
9589

9690

9791
def get_ks_version(sorter_path: Path) -> str:

examples/example_interactive.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
plot = plotter.drift_map_plot_interactive(
2222
decimate=False,
23-
exclude_noise=False,
2423
filter_amplitude_mode="percentile",
2524
filter_amplitude_values=(1, 99),
2625
amplitude_cmap_scaling="linear",

0 commit comments

Comments
 (0)