Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 21 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@ concurrency:
cancel-in-progress: true

jobs:
lint:
name: Lint
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.12"
cache: pip
cache-dependency-path: requirements-dev.txt

- name: Install lint dependencies
run: python -m pip install flake8

- name: Run lint
run: python -m flake8 phylib

test:
name: ${{ matrix.scope }} (${{ matrix.os }}, Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -66,7 +86,7 @@ jobs:
build:
name: Build package
runs-on: ubuntu-latest
needs: test
needs: [lint, test]

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down
25 changes: 16 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [2.7.1] 2026-09-09

### Added
- Code of Conduct for project participation and incident reporting.
- Added a Code of Conduct for project participation and incident reporting.

### Changed
- Optimized spike selection and stored sparse-waveform extraction for large datasets,
including bounded display sampling and batched memory-mapped reads.
- Unknown raw-data file extensions now emit a warning instead of preventing the dataset
from loading.

### Fixed
- Prevented geometry range searches from hanging with `float32` input.
- Development checkouts now report a development version with a Git commit suffix.
- Template datasets without waveform templates can be reloaded after cluster assignments change,
and stored spike-waveform subsets provide their waveform sample count.
- Cluster assignments are written atomically, so a crash during a save no longer truncates `spike_clusters.npy`.
- TSV, JSON, text and `params.py` files are written atomically, so a crash during a save no longer
truncates the file it was replacing, for instance `cluster_group.tsv`.
- #57 a blank `dat_path` in `params.py` is now read as "no raw data file" instead of resolving to
the dataset directory.
- Fixed reloading curated template-less datasets after cluster assignments change.
- Derived the waveform sample count from stored spike-waveform subsets when templates are
unavailable.
- Treated a blank `dat_path` in `params.py` as no raw-data file instead of the dataset
directory (#57).
- Made cluster-assignment, TSV, JSON, text, and `params.py` writes atomic to prevent
truncation if saving is interrupted.

## [2.7.0] 2025-12-10

Expand Down
2 changes: 1 addition & 1 deletion phylib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

__author__ = 'Cyrille Rossant'
__email__ = 'cyrille.rossant at gmail.com'
__version__ = '2.7.1.dev0'
__version__ = '2.7.1'
__version_git__ = __version__ + _git_version()


Expand Down
8 changes: 5 additions & 3 deletions phylib/io/alf.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,10 +373,11 @@ def get_waveforms_amp(templates):
cha = np.max(templates, axis=1) - np.min(templates, axis=1)
return np.max(cha, axis=1)

# we unwhiten the templates waveforms, this will expand the templates to the original non-sparse size
# Unwhitening expands the template waveforms to the original non-sparse size.
templates_phy = np.zeros([nclu, templates['waveforms'].shape[1], nch], dtype=np.float32)
for i in np.arange(templates_phy.shape[0]):
templates_phy[i] = np.matmul(templates['waveforms'][i], wm[templates['waveformsChannels'][i], :])
templates_phy[i] = np.matmul(
templates['waveforms'][i], wm[templates['waveformsChannels'][i], :])

# the original templates have a rms of 1.0, so here we just need to normalize by rms
rms_templates = np.sum(np.sum(templates['waveforms'] ** 2, axis=1), axis=1) ** 0.5
Expand All @@ -390,7 +391,8 @@ def get_waveforms_amp(templates):
np.save(target_path.joinpath('channel_map.npy'), np.arange(nch))
np.save(target_path.joinpath('templates.npy'), templates_phy)

np.save(target_path.joinpath('templates_ind.npy'), np.tile(np.arange(nclu)[np.newaxis, :], reps=[nch, 1]))
template_indices = np.tile(np.arange(nclu)[np.newaxis, :], reps=[nch, 1])
np.save(target_path.joinpath('templates_ind.npy'), template_indices)

# if we have metrics information, output the ks2_label information
if alf_path.joinpath('cluster.metrics.pqt').exists():
Expand Down
14 changes: 7 additions & 7 deletions phylib/io/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,14 +994,14 @@ def get_waveforms(self, spike_ids, channel_ids=None):
# Load from precomputed spikes.
try:
return get_spike_waveforms(
spike_ids, channel_ids, spike_waveforms=self.spike_waveforms,
n_samples_waveforms=nsw)
spike_ids, channel_ids, spike_waveforms=self.spike_waveforms,
n_samples_waveforms=nsw)
except AssertionError:
logger.warning(
"Error when loading waveforms from precomputed waveforms, trying to load the raw data.")
spike_samples = self.spike_samples[spike_ids]
return extract_waveforms(
self.traces, spike_samples, channel_ids, n_samples_waveforms=nsw)
logger.warning(
"Error when loading precomputed waveforms; trying the raw data instead.")
spike_samples = self.spike_samples[spike_ids]
return extract_waveforms(
self.traces, spike_samples, channel_ids, n_samples_waveforms=nsw)
else:
# Or load directly from raw data (slower).
spike_samples = self.spike_samples[spike_ids]
Expand Down
3 changes: 2 additions & 1 deletion phylib/io/tests/test_alf.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ def __init__(self, tempdir):
np.save(p / 'amplitudes.npy', nr.uniform(low=0.5, high=1.5, size=self.ns))
np.save(p / 'channel_positions.npy', np.c_[np.arange(self.nc), np.zeros(self.nc)])
templates = np.random.normal(size=(self.nt, 50, self.nc))
templates = templates / (np.sum(np.sum(templates ** 2, axis=1), axis=1) ** .5)[:, np.newaxis, np.newaxis]
template_norms = np.sum(np.sum(templates ** 2, axis=1), axis=1) ** .5
templates = templates / template_norms[:, np.newaxis, np.newaxis]
np.save(p / 'templates.npy', templates)
np.save(p / 'similar_templates.npy', np.tile(np.arange(self.nt), (self.nt, 1)))
np.save(p / 'channel_map.npy', np.c_[np.arange(self.nc)])
Expand Down
1 change: 1 addition & 0 deletions phylib/io/tests/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ def __getitem__(self, indices):
sampled = _sample_spikes_evenly(SortedSpikeIDs(), 10)
ae(sampled, np.linspace(0, 999_999, 10, dtype=np.int64))


def test_select_spikes_1():
spike_times = np.array([0., 1., 2., 3.3, 4.4])
spike_clusters = np.array([1, 2, 1, 2, 4])
Expand Down
4 changes: 2 additions & 2 deletions phylib/io/tests/test_datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,10 @@ def test_download_file(tempdir, mock_urls):

assert_succeeds = (data_here and data_valid and
((checksum_here == checksum_valid) or
(not(checksum_here) and checksum_valid)))
(not checksum_here and checksum_valid)))

download_succeeds = (assert_succeeds or (data_here and
(not(data_valid) and not(checksum_here))))
(not data_valid and not checksum_here)))

if download_succeeds:
data = _dl(path)
Expand Down
4 changes: 2 additions & 2 deletions phylib/utils/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ def silent(self):
"""Prevent all callbacks to be called if events are raised
in the context manager.
"""
self.is_silent = not(self.is_silent)
self.is_silent = not self.is_silent
yield
self.is_silent = not(self.is_silent)
self.is_silent = not self.is_silent

def connect(self, func=None, event=None, sender=None, **kwargs):
"""Register a callback function to a given event.
Expand Down