lfpack 0.2.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lfpack-0.2.0 → lfpack-0.4.0}/.gitignore +1 -0
- lfpack-0.4.0/CHANGELOG.md +62 -0
- lfpack-0.4.0/CLAUDE.md +97 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/PKG-INFO +24 -6
- {lfpack-0.2.0 → lfpack-0.4.0}/README.md +22 -4
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/_quarto.yml +11 -1
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/explanation/pipeline.qmd +3 -1
- lfpack-0.4.0/docs/figures/bwm_compression_hf_rolloff.png +0 -0
- lfpack-0.4.0/docs/figures/bwm_saturation_detection.png +0 -0
- lfpack-0.4.0/docs/figures/viewephys_recording_selector.jpg +0 -0
- lfpack-0.4.0/docs/how-to/bwm-dataset.qmd +272 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/how-to/multi-recording.qmd +1 -16
- lfpack-0.4.0/docs/how-to/write-your-own-file.qmd +108 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/index.qmd +1 -0
- lfpack-0.4.0/docs/objects.json +1 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/LFPackReader.qmd +31 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/_sidebar.yml +4 -0
- lfpack-0.4.0/docs/reference/compress.qmd +29 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/compress_bin_to_h5.qmd +25 -19
- lfpack-0.4.0/docs/reference/compress_to_h5.qmd +76 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/hdf5-layout.qmd +42 -1
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/index.qmd +10 -1
- lfpack-0.4.0/docs/reference/merge_h5.qmd +31 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/run_cadzow_checkpoint.qmd +2 -0
- lfpack-0.4.0/docs/reference/subset_h5.qmd +33 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/tutorials/first-compression.qmd +1 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/pyproject.toml +1 -1
- {lfpack-0.2.0 → lfpack-0.4.0}/src/lfpack/__init__.py +2 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/src/lfpack/_core.py +408 -34
- lfpack-0.4.0/src/lfpack/_sync.py +124 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/tests/test_lfpack.py +411 -3
- lfpack-0.2.0/CHANGELOG.md +0 -17
- lfpack-0.2.0/CLAUDE.md +0 -77
- lfpack-0.2.0/docs/how-to/bwm-dataset.qmd +0 -157
- lfpack-0.2.0/docs/objects.json +0 -1
- lfpack-0.2.0/docs/reference/compress.qmd +0 -21
- lfpack-0.2.0/docs/reference/compress_to_h5.qmd +0 -56
- {lfpack-0.2.0 → lfpack-0.4.0}/.githooks/pre-commit +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/.github/workflows/docs.yml +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/.github/workflows/publish_to_pypi.yaml +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/.github/workflows/tests.yml +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/CONTRIBUTING.md +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/.gitignore +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/README.txt +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/explanation/gallery.qmd +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/density_lfp_dab512bd.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/favicon.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-02_lfp_density_voltage_1a276285-8b0e-4cc9-9f0a-a3a002978724.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-02_lfp_density_voltage_1e104bf4-7a24-4624-a5b2-c2c8289c0de7.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-02_lfp_density_voltage_6638cfb3-3831-4fc2-9327-194b76cf22e1.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-02_lfp_density_voltage_749cb2b7-e57e-4453-a794-f6230e4d0226.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-02_lfp_density_voltage_d7ec0892-0a6c-4f4f-9d8f-72083692af5c.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-02_lfp_density_voltage_da8dfec1-d265-44e8-84ce-6ae9c109b8bd.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-02_lfp_density_voltage_dab512bd-a02d-4c1f-8dbc-9155a163efc0.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-02_lfp_density_voltage_dc7e9403-19f7-409f-9240-05ee57cb7aea.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-02_lfp_density_voltage_e8f9fba4-d151-4b00-bee7-447f0f3e752c.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-02_lfp_density_voltage_eebcaf65-7fa4-4118-869d-a084e84530e2.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-02_lfp_density_voltage_fe380793-8035-414e-b000-09bfe5ece92a.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-15_lfp_compression_aggregate.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/lfp_binned_comparison.jpg +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/logo.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/viewephys_screenshot.jpg +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/viewephys_screenshot.png +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/how-to/binned-reads.qmd +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/LFPCompressed.qmd +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/compress_pipeline.qmd +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/decompress.qmd +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/docs/references.bib +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/tests/__init__.py +0 -0
- {lfpack-0.2.0 → lfpack-0.4.0}/uv.lock +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.4.0] - 2026-08-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Non-linear sync support: `sync_samples`/`sync_times` — the raw ALF sync knot pairs
|
|
14
|
+
(sample index <-> time), stored verbatim per scale — alongside the existing
|
|
15
|
+
`t0_sync`/`fs_sync` scalars, which are now always a least-squares affine derived from
|
|
16
|
+
the knots (or the legacy scalar, for archives predating this change). `LFPackReader`
|
|
17
|
+
interpolates through the knots for `.times`/`saturation_times()` inside their range and
|
|
18
|
+
falls back to the derived affine outside it (`interp1d` with no extrapolation, rather
|
|
19
|
+
than the less stable slope of the two outermost knots). New `lfpack.write_sync_attrs` /
|
|
20
|
+
`lfpack.clear_sync_attrs` (`src/lfpack/_sync.py`) write/clear both tiers together and
|
|
21
|
+
validate the knots (>=2 points, strictly increasing, all-finite). `sync_samples`/
|
|
22
|
+
`sync_times` are stored as gzip+shuffle child datasets of `meta`, not attrs — a
|
|
23
|
+
`type='exact'` fit's knots (every raw pulse, verbatim) can run to tens of thousands of
|
|
24
|
+
points, well past HDF5's per-attribute object-header-message size limit.
|
|
25
|
+
- Added `docs/how-to/write-your-own-file.qmd`, documenting the two sync styles (linear
|
|
26
|
+
affine vs. piecewise knots) and moving/fixing the multi-recording write example that
|
|
27
|
+
used to live in `how-to/multi-recording.qmd`.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- `saturation_times()` now calls the same sample→time conversion as `.times` instead of
|
|
31
|
+
duplicating the affine formula inline, so the two could never silently diverge on a
|
|
32
|
+
scale with real non-linear sync structure.
|
|
33
|
+
|
|
34
|
+
## [0.3.0] - 2026-07-25
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- `subset_h5` — copy a subset of recordings out of a multi-recording HDF5 archive
|
|
38
|
+
(inverse of `merge_h5`), for carving a smaller release (e.g. BWM) out of a larger
|
|
39
|
+
superset (e.g. ephys-atlas) without re-compression.
|
|
40
|
+
- Bad-channel labels (0=good, 1=dead, 2=noisy, 3=outside brain) persisted through
|
|
41
|
+
compression and exposed via `LFPackReader.channels` / `channels_full`.
|
|
42
|
+
- **Saturation detection and muting**: ADC-clipped spans are detected on the raw LFP
|
|
43
|
+
band, stored as a per-recording interval table, and muted (cosine-tapered zero) on
|
|
44
|
+
the decimated output after Cadzow denoising. Exposed via `LFPackReader.saturation`
|
|
45
|
+
(raw-rate sample indices, recording-aligned) and `.saturation_summary`.
|
|
46
|
+
`.saturation_mask` is a property, indexed like the reader itself (`sr.saturation_mask[a:b]`);
|
|
47
|
+
`.saturation_times()` converts samples to session-clock seconds (matching `times`).
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
- `compress` no longer decompresses low-SNR chunks to exact zero; new `floor_k=64`
|
|
51
|
+
survival floor keeps the dominant mode's largest coefficients. Closes #2.
|
|
52
|
+
|
|
53
|
+
## [0.2.0] - 2026-06-30
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
- `LFPackReader.channels` and `LFPackReader.channels_full` — per-channel probe geometry
|
|
57
|
+
and brain location annotations (`x/y/z` MNI coordinates, `atlas_id`, `acronym`).
|
|
58
|
+
Brain location fields are optional; the properties work on any existing file.
|
|
59
|
+
`channels` aggregates over binned-channel groups (mean for coordinates, mode for
|
|
60
|
+
brain region); `channels_full` always returns the raw per-electrode data.
|
|
61
|
+
- `compress_to_h5` accepts an optional `channels` dict to embed brain location
|
|
62
|
+
annotations at compression time.
|
lfpack-0.4.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this project is
|
|
6
|
+
|
|
7
|
+
**lfpack** is a lossy LFP (local-field-potential) codec for Neuropixels recordings. It compresses raw `.cbin` LFP binaries into HDF5 files at >100× compression with RMSE < 25 µV, using an 8-stage pipeline followed by adaptive SVD + wavelet-packet thresholding.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Dev setup
|
|
13
|
+
uv sync --group dev
|
|
14
|
+
git config core.hooksPath .githooks # installs ruff pre-commit hook
|
|
15
|
+
|
|
16
|
+
# Tests
|
|
17
|
+
uv run pytest # all tests
|
|
18
|
+
uv run pytest tests/test_lfpack.py::TestLfpack::test_compress_output_shapes # single test
|
|
19
|
+
|
|
20
|
+
# Lint / format
|
|
21
|
+
uv run ruff check src/ tests/
|
|
22
|
+
uv run ruff format src/ tests/
|
|
23
|
+
|
|
24
|
+
# Docs — regenerate API reference from docstrings, then preview
|
|
25
|
+
uv run quartodoc build --config docs/_quarto.yml
|
|
26
|
+
quarto preview docs/
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Architecture
|
|
30
|
+
|
|
31
|
+
Nearly all code lives in `src/lfpack/_core.py` (~1700 lines). Non-linear sync knot storage (`write_sync_attrs`/`clear_sync_attrs`) lives in the small `src/lfpack/_sync.py` module. The public API is re-exported from `src/lfpack/__init__.py`.
|
|
32
|
+
|
|
33
|
+
### Compression pipeline (`compress_bin_to_h5`)
|
|
34
|
+
|
|
35
|
+
0. Saturation detection — flag ADC-clipped samples on the raw LFP band *before* any step below obscures them (`ibldsp.voltage.saturation_cbin`, parallel over `n_jobs`). Detection is **amplitude-only** for LFP (`v_per_sec=None`): the derivative criterion is tuned for the 30 kHz AP band and mislabels normal LFP dynamics as saturation, so it is disabled here. Stored as an insertion-level interval table (see HDF5 layout) and used to mute the clipped stretches; toggle with `detect_saturation` / `saturation_kwargs`.
|
|
36
|
+
1. Bad-channel detection (`ibldsp.voltage.detect_bad_channels_cbin`) — labels saved to the scale-`00` `meta` as a `labels` int8 attr; read via `LFPackReader.channels`/`channels_full` under the `labels` key.
|
|
37
|
+
2. Dephasing — sample-shift correction (NP1 only, via `ibldsp.fourier.fshift`)
|
|
38
|
+
3. Highpass filter — 0.5 Hz zero-phase 3rd-order Butterworth (keeps delta/infra-slow; ibldsp warmup padding scales with the corner). The true recording start/end are cosine-apodized (~3 filter time-constants) before this filter so the zero-phase transient cannot ring against the data-boundary step (interior chunk edges are handled by the warmup padding).
|
|
39
|
+
4. Bad-channel interpolation — distance-weighted neighbors
|
|
40
|
+
5. CAR — median subtraction across channels
|
|
41
|
+
6. Decimation — 2500 → 250 Hz (Q=10)
|
|
42
|
+
7. Cadzow denoising — spatial rank reduction (`ibldsp.cadzow`)
|
|
43
|
+
8. **Adaptive SVD + wavelet-packet thresholding** — the actual codec
|
|
44
|
+
|
|
45
|
+
Steps 6–7 are checkpointed to a `.npy` file so the expensive decimation can be skipped on resume. Steps 7–8 are parallelised with `joblib`. Muting (step 0): a raw-rate boolean saturation mask is passed to `resample_denoise_lfp_cbin` as `saturation_file`; each worker downsamples its slice of the mask onto the decimated grid (block-max over each Q-sample block), cosine-tapers it (`mute_window_samples`, now in **decimated** output samples) and multiplies the **decimated output *after* Cadzow** by it. This *late* muting replaced the earlier pre-filter approach: muting the raw traces before the highpass/CAR/decimate/Cadzow did not keep the span clean — those stages leaked energy back into it (residual noise, spurious CSD sources/sinks); a single re-mute of the final output is cleaner and cheaper. Muting is skipped when resuming from an existing checkpoint (data already decimated); the interval table is still written.
|
|
46
|
+
|
|
47
|
+
### Codec (`compress` / `decompress`)
|
|
48
|
+
|
|
49
|
+
`compress(data, epsilon=150, alpha=28)` → `LFPCompressed` dataclass:
|
|
50
|
+
- **SVD rank selection**: keep singular values > `epsilon × sigma_noise`
|
|
51
|
+
- **Wavelet-packet thresholding**: db4, level 5; per-component threshold `alpha × sigma_noise / sv[k]`
|
|
52
|
+
- Sparse Vh stored as `(vh_indices, vh_values)` in HDF5; `U_scaled` with shuffle+gzip
|
|
53
|
+
|
|
54
|
+
Guard bands: 64-sample Cadzow halos, 128-sample SVD/WP overlap to prevent edge transients.
|
|
55
|
+
|
|
56
|
+
### HDF5 layout (multi-recording, pyramidal)
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
<file>.h5
|
|
60
|
+
└─ <recording>/
|
|
61
|
+
├─ saturation # (n_intervals, 2) int64 [start_sample, stop_sample] at raw LFP fs;
|
|
62
|
+
│ # written once per recording (scale-independent). attrs: fs, ns_total,
|
|
63
|
+
│ # n_saturated_samples, saturated_fraction, detection params, muted
|
|
64
|
+
└─ <scale_2digit>/
|
|
65
|
+
├─ meta # attrs: nc, ns_total, fs, fs_sync, t0_sync, epsilon, alpha, geometry, …
|
|
66
|
+
│ # optional child datasets: sync_samples, sync_times (gzip+shuffle)
|
|
67
|
+
└─ chunks/
|
|
68
|
+
└─ <i>/ # U_scaled, vh_indices, vh_values + attrs
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The `saturation` node sits at recording level (not under a scale) because it describes the raw recording, not a codec pyramid level; `merge_h5` copies it automatically. Legacy flat layout (meta at root) is still readable for backwards compatibility.
|
|
72
|
+
|
|
73
|
+
`fs_sync`/`t0_sync` are always plain scalar attrs — a least-squares affine (derived from
|
|
74
|
+
`sync_samples`/`sync_times` when present, else the legacy scalar sync, else the nominal
|
|
75
|
+
rate) — so downstream consumers reading `meta.attrs` directly (e.g. `qc_report.py`) and
|
|
76
|
+
`LFPackReader.t0`/`.fs` never see anything but a scalar. `sync_samples`/`sync_times`
|
|
77
|
+
(float64, optional) are the raw ALF sync knot pairs (sample index <-> time) in this
|
|
78
|
+
scale's own native sample-index units, written verbatim with no re-fitting; `LFPackReader`
|
|
79
|
+
interpolates through them for `.times`/`saturation_times()` inside their range and falls
|
|
80
|
+
back to the derived affine outside it. Stored as **child datasets** of `meta`, not attrs
|
|
81
|
+
like everything else here — a `type='exact'` fit keeps every raw pulse verbatim, which can
|
|
82
|
+
run to tens of thousands of knots and overflows HDF5's per-attribute object-header-message
|
|
83
|
+
size limit (`OSError: object header message is too large`, hit at ~38k knots in production).
|
|
84
|
+
Write/clear both tiers together via `lfpack.write_sync_attrs(meta_group, sample_knots,
|
|
85
|
+
time_knots)` / `lfpack.clear_sync_attrs(meta_group)` (in `src/lfpack/_sync.py`) rather than
|
|
86
|
+
setting them by hand.
|
|
87
|
+
|
|
88
|
+
### `LFPackReader`
|
|
89
|
+
|
|
90
|
+
Drop-in replacement for `spikeglx.Reader`. Wraps an HDF5 file and decompresses chunks on demand. Supports multi-recording files via `recording=` kwarg and pyramidal scales via `scale=` kwarg. Saturation access: `.saturation` (interval DataFrame with raw samples + seconds), `.saturation_summary` (fraction/count/muted from attrs), `saturation_mask(first, last)` (boolean at the reader's decimated rate, interval edges rounded outward). All three degrade gracefully to empty/default on files written without saturation detection.
|
|
91
|
+
|
|
92
|
+
## Ecosystem interconnections
|
|
93
|
+
|
|
94
|
+
lfpack sits in a tight three-way dependency with two sibling IBL packages:
|
|
95
|
+
|
|
96
|
+
- **ibl-neuropixel** (`/Users/olivier/PycharmProjects/ephys-atlas/ibl-neuropixel`) — provides the low-level Neuropixels binary I/O (`spikeglx.Reader`), destriping, dephasing (`ibldsp.fourier.fshift`), decimation (`ibldsp.voltage.resample_denoise_lfp_cbin`), bad-channel detection (`ibldsp.voltage.detect_bad_channels_cbin`), saturation detection (`ibldsp.voltage.saturation_cbin` / `saturation` / `saturation_samples_to_intervals`), and Cadzow denoising (`ibldsp.cadzow`). lfpack wraps these directly; changes to ibldsp can break the pipeline. `resample_denoise_lfp_cbin` gained a `saturation_file` / `mute_window_samples` parameter (added for this codec) to late-mute saturated stretches on the decimated output after Cadzow.
|
|
97
|
+
- **viewephys** (`/Users/olivier/PycharmProjects/ephys-atlas/viewephys`) — Qt-based interactive viewer for raw Neuropixels traces. Since [PR #49](https://github.com/int-brain-lab/viewephys/pull/49) it has a native lfpack backend (`LFPackDataModel` / `LFPackBinViewer`, optional `viewephys[lfpack]` extra): `viewephys -f file.h5` opens a `.h5` file directly, with automatic brain-region colouring from embedded `atlas_id`/`acronym` annotations, a searchable multi-recording selector, and a CSD step. No manual transpose or `BrainRegions` wiring needed.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: lfpack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: LFP codec — lossy compression of local-field-potential recordings via adaptive SVD and wavelet-packet thresholding
|
|
5
5
|
Project-URL: Homepage, https://github.com/int-brain-lab/lfpack
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/int-brain-lab/lfpack/issues
|
|
@@ -19,9 +19,6 @@ Description-Content-Type: text/markdown
|
|
|
19
19
|
|
|
20
20
|
# lfpack — LFP codec for Neuropixels recordings
|
|
21
21
|
|
|
22
|
-
> **IBL Brain-Wide Map LFP dataset** — 699 recordings, 384 channels, session-clock aligned.
|
|
23
|
-
> [How to access →](https://int-brain-lab.github.io/lfpack/how-to/bwm-dataset.html)
|
|
24
|
-
|
|
25
22
|
<p align="center">
|
|
26
23
|
<img src="docs/figures/logo.png" alt="lfpack logo" width="300"/>
|
|
27
24
|
</p>
|
|
@@ -30,10 +27,25 @@ Lossy codec for local-field-potential (LFP) recordings from Neuropixels probes.
|
|
|
30
27
|
Achieves **>100× compression** with median RMSE < 25 µV via an 8-stage pipeline
|
|
31
28
|
(bad-channel detection → dephasing → highpass → interpolation → CAR → decimation → Cadzow → adaptive SVD + wavelet-packet thresholding).
|
|
32
29
|
|
|
30
|
+
> **IBL Brain-Wide Map LFP dataset** — 699 recordings, 384 channels, session-clock aligned.
|
|
31
|
+
> [How to access →](https://int-brain-lab.github.io/lfpack/how-to/bwm-dataset.html)
|
|
32
|
+
|
|
33
33
|
```bash
|
|
34
34
|
pip install lfpack
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
## Visualise with viewephys
|
|
38
|
+
|
|
39
|
+
[`viewephys`](https://github.com/int-brain-lab/viewephys) opens lfpack `.h5` files natively —
|
|
40
|
+
`viewephys -f recording.h5` gets you a browsable, brain-region-coloured view with no
|
|
41
|
+
manual decompression step:
|
|
42
|
+
|
|
43
|
+
<p align="center">
|
|
44
|
+
<img src="docs/figures/viewephys_screenshot.jpg" alt="viewephys opened directly on a lfpack HDF5 file, showing brain-region-coloured LFP traces" width="700"/>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
See the [BWM how-to](https://int-brain-lab.github.io/lfpack/how-to/bwm-dataset.html#visualise-with-viewephys) for details.
|
|
48
|
+
|
|
37
49
|
## Documentation
|
|
38
50
|
|
|
39
51
|
Full documentation is at **https://int-brain-lab.github.io/lfpack/**.
|
|
@@ -46,4 +58,10 @@ Full documentation is at **https://int-brain-lab.github.io/lfpack/**.
|
|
|
46
58
|
| [API reference](https://int-brain-lab.github.io/lfpack/reference/) | Full public API (`compress_bin_to_h5`, `LFPackReader`, …) |
|
|
47
59
|
| [HDF5 format](https://int-brain-lab.github.io/lfpack/reference/hdf5-layout.html) | On-disk layout specification |
|
|
48
60
|
| [Pipeline explanation](https://int-brain-lab.github.io/lfpack/explanation/pipeline.html) | Stage-by-stage description of the compression pipeline |
|
|
49
|
-
| [SVD+WP benchmark](https://int-brain-lab.github.io/lfpack/explanation/benchmark.html) | RMSE, SNR, and compression-ratio results across 11 insertions |
|
|
61
|
+
| [SVD+WP benchmark](https://int-brain-lab.github.io/lfpack/explanation/benchmark.html) | RMSE, SNR, and compression-ratio results across 11 insertions |
|
|
62
|
+
|
|
63
|
+
## Help and Feedback
|
|
64
|
+
|
|
65
|
+
Have a question, found an issue, or want to share feedback? Please open an issue on
|
|
66
|
+
[GitHub](https://github.com/int-brain-lab/lfpack/issues). We welcome bug reports, questions,
|
|
67
|
+
suggestions, and other feedback.
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# lfpack — LFP codec for Neuropixels recordings
|
|
2
2
|
|
|
3
|
-
> **IBL Brain-Wide Map LFP dataset** — 699 recordings, 384 channels, session-clock aligned.
|
|
4
|
-
> [How to access →](https://int-brain-lab.github.io/lfpack/how-to/bwm-dataset.html)
|
|
5
|
-
|
|
6
3
|
<p align="center">
|
|
7
4
|
<img src="docs/figures/logo.png" alt="lfpack logo" width="300"/>
|
|
8
5
|
</p>
|
|
@@ -11,10 +8,25 @@ Lossy codec for local-field-potential (LFP) recordings from Neuropixels probes.
|
|
|
11
8
|
Achieves **>100× compression** with median RMSE < 25 µV via an 8-stage pipeline
|
|
12
9
|
(bad-channel detection → dephasing → highpass → interpolation → CAR → decimation → Cadzow → adaptive SVD + wavelet-packet thresholding).
|
|
13
10
|
|
|
11
|
+
> **IBL Brain-Wide Map LFP dataset** — 699 recordings, 384 channels, session-clock aligned.
|
|
12
|
+
> [How to access →](https://int-brain-lab.github.io/lfpack/how-to/bwm-dataset.html)
|
|
13
|
+
|
|
14
14
|
```bash
|
|
15
15
|
pip install lfpack
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Visualise with viewephys
|
|
19
|
+
|
|
20
|
+
[`viewephys`](https://github.com/int-brain-lab/viewephys) opens lfpack `.h5` files natively —
|
|
21
|
+
`viewephys -f recording.h5` gets you a browsable, brain-region-coloured view with no
|
|
22
|
+
manual decompression step:
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="docs/figures/viewephys_screenshot.jpg" alt="viewephys opened directly on a lfpack HDF5 file, showing brain-region-coloured LFP traces" width="700"/>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
See the [BWM how-to](https://int-brain-lab.github.io/lfpack/how-to/bwm-dataset.html#visualise-with-viewephys) for details.
|
|
29
|
+
|
|
18
30
|
## Documentation
|
|
19
31
|
|
|
20
32
|
Full documentation is at **https://int-brain-lab.github.io/lfpack/**.
|
|
@@ -27,4 +39,10 @@ Full documentation is at **https://int-brain-lab.github.io/lfpack/**.
|
|
|
27
39
|
| [API reference](https://int-brain-lab.github.io/lfpack/reference/) | Full public API (`compress_bin_to_h5`, `LFPackReader`, …) |
|
|
28
40
|
| [HDF5 format](https://int-brain-lab.github.io/lfpack/reference/hdf5-layout.html) | On-disk layout specification |
|
|
29
41
|
| [Pipeline explanation](https://int-brain-lab.github.io/lfpack/explanation/pipeline.html) | Stage-by-stage description of the compression pipeline |
|
|
30
|
-
| [SVD+WP benchmark](https://int-brain-lab.github.io/lfpack/explanation/benchmark.html) | RMSE, SNR, and compression-ratio results across 11 insertions |
|
|
42
|
+
| [SVD+WP benchmark](https://int-brain-lab.github.io/lfpack/explanation/benchmark.html) | RMSE, SNR, and compression-ratio results across 11 insertions |
|
|
43
|
+
|
|
44
|
+
## Help and Feedback
|
|
45
|
+
|
|
46
|
+
Have a question, found an issue, or want to share feedback? Please open an issue on
|
|
47
|
+
[GitHub](https://github.com/int-brain-lab/lfpack/issues). We welcome bug reports, questions,
|
|
48
|
+
suggestions, and other feedback.
|
|
@@ -16,6 +16,8 @@ website:
|
|
|
16
16
|
file: tutorials/first-compression.qmd
|
|
17
17
|
- text: "How-To"
|
|
18
18
|
menu:
|
|
19
|
+
- text: "Write your own file"
|
|
20
|
+
file: how-to/write-your-own-file.qmd
|
|
19
21
|
- text: "Binned-channel reads"
|
|
20
22
|
file: how-to/binned-reads.qmd
|
|
21
23
|
- text: "Multi-recording files"
|
|
@@ -33,6 +35,9 @@ website:
|
|
|
33
35
|
- text: "Benchmark gallery"
|
|
34
36
|
file: explanation/gallery.qmd
|
|
35
37
|
right:
|
|
38
|
+
- text: "Help"
|
|
39
|
+
icon: question-circle
|
|
40
|
+
href: https://github.com/int-brain-lab/lfpack/issues
|
|
36
41
|
- icon: github
|
|
37
42
|
href: https://github.com/int-brain-lab/lfpack
|
|
38
43
|
|
|
@@ -65,4 +70,9 @@ quartodoc:
|
|
|
65
70
|
- title: "Reader"
|
|
66
71
|
desc: "Random-access decompression."
|
|
67
72
|
contents:
|
|
68
|
-
- LFPackReader
|
|
73
|
+
- LFPackReader
|
|
74
|
+
- title: "Multi-recording archives"
|
|
75
|
+
desc: "Combine or subset recordings across HDF5 files without re-compression."
|
|
76
|
+
contents:
|
|
77
|
+
- merge_h5
|
|
78
|
+
- subset_h5
|
|
@@ -139,7 +139,9 @@ This ensures dominant spatial modes receive finer temporal detail than weak mode
|
|
|
139
139
|
|
|
140
140
|
Non-zero coefficients are stored as sparse index/value pairs in HDF5.
|
|
141
141
|
|
|
142
|
-
**
|
|
142
|
+
**Survival floor.** Because each row of $V_r^\top$ has unit norm, its wavelet-packet coefficients have a fixed magnitude ceiling ($\max_j|w_{kj}| \approx 0.2\text{–}0.4$). On low-SNR chunks — where even the dominant mode is weak, $s_0/\sigma_\text{noise} \lesssim \alpha/\max_j|w_{0j}| \approx 140$ — the threshold exceeds every coefficient and the whole chunk reconstructs to exact zero, silently discarding real low-amplitude LFP. To prevent this the dominant mode ($k=0$) always keeps its `floor_k` largest coefficients. The floor is a no-op on high-SNR recordings (their dominant row keeps far more than `floor_k`, so their output is bit-for-bit unchanged) and on saturation-muted spans, which enter as all-zero and still reconstruct to zero ($s_0 = 0 \Rightarrow$ `U_scaled` $= 0$).
|
|
143
|
+
|
|
144
|
+
**Parameters:** `epsilon` — SVD noise threshold multiplier (default `150`); `alpha` — WP threshold multiplier (default `28`); `floor_k` — minimum WP coefficients kept on the dominant mode (default `64`)
|
|
143
145
|
|
|
144
146
|
### Guard bands
|
|
145
147
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "How to access the IBL Brain-Wide Map LFP dataset"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
The IBL Brain-Wide Map (BWM) LFP dataset is distributed as a 16GB `.h5` file
|
|
6
|
+
containing 699 individual recordings of 384 channels each.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
::: {.callout-warning}
|
|
10
|
+
## Known Dataset limitations
|
|
11
|
+
|
|
12
|
+
- **Saturation events** — ADC-clipped stretches are flagged and muted in the decompressed
|
|
13
|
+
output; check whether your window of interest overlaps one before trusting amplitude.
|
|
14
|
+
See [Quality Control - Saturation](#quality-control---saturation).
|
|
15
|
+
- **Missing sync for 7 probes** — `t0_sync`/`fs_sync` could not be computed for 7 probes
|
|
16
|
+
(spanning 4 sessions) due to a session-level sync data-quality issue upstream; `sr.t0`
|
|
17
|
+
returns `NaN` for these. See [lfpack#8](https://github.com/int-brain-lab/lfpack/issues/8).
|
|
18
|
+
- **High-frequency roll-off** — the SVD + wavelet-packet codec trades off power above
|
|
19
|
+
roughly 20-30 Hz for compression ratio, more so at the aggressive tier. See the PSD
|
|
20
|
+
comparison below.
|
|
21
|
+
- **Low-frequency channel-to-channel mismatch** — per-channel impedance/analog-filter
|
|
22
|
+
differences show up as amplitude outliers at low frequencies (below 1.5 Hz) and are not fully
|
|
23
|
+
corrected by the current pre-processing pipeline.
|
|
24
|
+
|
|
25
|
+
{.lightbox}
|
|
26
|
+
:::
|
|
27
|
+
|
|
28
|
+
::: {.callout-important}
|
|
29
|
+
## Before you start
|
|
30
|
+
|
|
31
|
+
- **Align with trial events via `sr.times`** — each recording stores a sync-corrected
|
|
32
|
+
session clock; always use `sr.times` rather than a manual `np.arange / sr.fs` when
|
|
33
|
+
matching LFP samples to spike times or trial events.
|
|
34
|
+
See [Session-clock times](#session-clock-times).
|
|
35
|
+
|
|
36
|
+
- **Use binned reads for brainwide sweeps** — looping over all 699 recordings at full
|
|
37
|
+
384-channel resolution is memory-intensive. Pass `bin_channels=4` to reduce each
|
|
38
|
+
recording to 96 spatial bins with acceptable loss of depth coverage.
|
|
39
|
+
See [Binned-channel reads](#binned-channel-reads).
|
|
40
|
+
:::
|
|
41
|
+
|
|
42
|
+
## Visualise with viewephys
|
|
43
|
+
|
|
44
|
+
`viewephys` has a native lfpack backend — point it at the `.h5` file directly,
|
|
45
|
+
no manual transpose or `BrainRegions` wiring needed:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
viewephys -f lf_compressed_all_bwm.h5
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The `.h5` extension is auto-detected and opens the lfpack-aware viewer, which:
|
|
52
|
+
|
|
53
|
+
- shows a searchable recording selector (type any `pid` substring) when the
|
|
54
|
+
file holds multiple recordings, preserving window size, position and zoom
|
|
55
|
+
when you switch
|
|
56
|
+
- colours the depth axis by brain region automatically, reading the
|
|
57
|
+
`atlas_id` / `acronym` annotations embedded in the file — no separate
|
|
58
|
+
`iblatlas` call required
|
|
59
|
+
- adds a **CSD** (current-source density) step alongside the raw signal
|
|
60
|
+
|
|
61
|
+
{fig-alt="Ephys Bin Viewer window showing the searchable recording dropdown and raw/CSD checkboxes for an lfpack HDF5 file" .lightbox}
|
|
62
|
+
|
|
63
|
+
The same class is usable from Python:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from viewephys.gui import LFPackBinViewer
|
|
67
|
+
from viewephys.viewer.qt import create_app
|
|
68
|
+
|
|
69
|
+
app = create_app()
|
|
70
|
+
win = LFPackBinViewer("lf_compressed_all_bwm.h5")
|
|
71
|
+
app.exec()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
{fig-alt="viewephys density view showing 384-channel LFP data with brain region colour bar and a searchable recording selector" .lightbox}
|
|
75
|
+
|
|
76
|
+
See [viewephys PR #49](https://github.com/int-brain-lab/viewephys/pull/49) for implementation details.
|
|
77
|
+
|
|
78
|
+
## Prerequisites
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv pip install "viewephys[lfpack]" one-api
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Download
|
|
85
|
+
|
|
86
|
+
From inside the `ibl-ai-agent` repository:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv run python scripts/download_datasets.py --lfp
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This downloads `lf_compressed_all_bwm.h5` (~14 GB) into `reports/datasets/bwm_lfp/`
|
|
93
|
+
and writes the path into `data_locations.local.yaml` automatically.
|
|
94
|
+
|
|
95
|
+
Or, from Python (no AWS credentials needed — uses the ONE public S3 helper):
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from one.remote.aws import s3_download_file
|
|
99
|
+
|
|
100
|
+
s3_download_file(
|
|
101
|
+
source="resources/ibl-agent-data/lf_compressed_all_bwm.h5",
|
|
102
|
+
destination="lf_compressed_all_bwm.h5",
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`s3_download_file` defaults to the IBL public bucket, shows a tqdm progress bar,
|
|
107
|
+
and skips the download if the local file already has the correct size.
|
|
108
|
+
|
|
109
|
+
## Session-clock times
|
|
110
|
+
|
|
111
|
+
`sr.times` returns a `(ns,)` array of session-clock timestamps (seconds) for every
|
|
112
|
+
LFP sample, derived from the sync signal stored during compression.
|
|
113
|
+
Use it to align LFP traces with trial events or spike times:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from lfpack import LFPackReader
|
|
117
|
+
import numpy as np
|
|
118
|
+
|
|
119
|
+
file_lfpack = "lf_compressed_all_bwm.h5"
|
|
120
|
+
pid = 'dab512bd-a02d-4c1f-8dbc-9155a163efc0'
|
|
121
|
+
|
|
122
|
+
sr = LFPackReader(file_lfpack, recording=pid)
|
|
123
|
+
|
|
124
|
+
n = int(10 * sr.fs)
|
|
125
|
+
traces = sr[:n, :] # (n_samples, nc) — first 10 s
|
|
126
|
+
t = sr.times[:n] # (n_samples,) — session-clock seconds
|
|
127
|
+
|
|
128
|
+
# Align with trial events loaded via ONE
|
|
129
|
+
# stim_on = trials['stimOn_times'] # already in session-clock seconds
|
|
130
|
+
# idx = np.searchsorted(t, stim_on) # nearest LFP sample for each stimulus onset
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## List recordings
|
|
134
|
+
|
|
135
|
+
Each file contains one entry per BWM probe. Recording identifiers are named after the probe identifier UUID `pid`.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from lfpack import LFPackReader
|
|
139
|
+
|
|
140
|
+
recordings = LFPackReader.recordings("lf_compressed_all_bwm.h5")
|
|
141
|
+
print(f"{len(recordings)} recordings")
|
|
142
|
+
print(recordings[:3])
|
|
143
|
+
# 699 recordings
|
|
144
|
+
# ['00a824c0-e060-495f-9ebc-79c82fef4c67', '00a96dee-1e8b-44cc-9cc3-aca704d2b594', '00c425fd-ec3e-4cd2-b8af-c0bc0c4bdd44']
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Read a single recording
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from lfpack import LFPackReader
|
|
151
|
+
|
|
152
|
+
file_lfpack = "lf_compressed_all_bwm.h5"
|
|
153
|
+
pid = 'dab512bd-a02d-4c1f-8dbc-9155a163efc0'
|
|
154
|
+
|
|
155
|
+
sr = LFPackReader(file_lfpack, recording=pid)
|
|
156
|
+
|
|
157
|
+
# First 10 seconds — shape (2500, nc), float32, volts
|
|
158
|
+
traces = sr[: int(10 * sr.fs)].T
|
|
159
|
+
|
|
160
|
+
print(f"Duration: {sr.ns / sr.fs:.1f} s")
|
|
161
|
+
print(f"Channels: {sr.nc}")
|
|
162
|
+
print(f"Sample rate: {sr.fs} Hz") # 250 Hz (decimated from 2500 Hz)
|
|
163
|
+
print(f"Shape: {traces.shape}")
|
|
164
|
+
|
|
165
|
+
# Duration: 3668.9 s
|
|
166
|
+
# Channels: 384
|
|
167
|
+
# Sample rate: 250.00252518761928 Hz
|
|
168
|
+
# Shape: (384, 2500)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`LFPackReader` is a drop-in for `spikeglx.Reader`.
|
|
172
|
+
Slicing decompresses only the requested chunks — the full file is never loaded into memory.
|
|
173
|
+
|
|
174
|
+
## Quality Control - Saturation
|
|
175
|
+
|
|
176
|
+
Each recording carries an insertion-level table of ADC-saturated (clipped) spans,
|
|
177
|
+
detected on the raw LFP band before any filtering. The saturated stretches are muted
|
|
178
|
+
(zeroed) in the decompressed output, so it's worth checking whether a window of
|
|
179
|
+
interest overlaps one before trusting the amplitude.
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from lfpack import LFPackReader
|
|
183
|
+
|
|
184
|
+
sr = LFPackReader("lf_compressed_all_bwm.h5", recording=pid)
|
|
185
|
+
|
|
186
|
+
# Interval table: raw-rate sample indices, recording-aligned
|
|
187
|
+
sr.saturation
|
|
188
|
+
# start_sample stop_sample
|
|
189
|
+
# 0 123456 125000
|
|
190
|
+
|
|
191
|
+
# Recording-level summary (fraction/count/whether muting was applied)
|
|
192
|
+
sr.saturation_summary
|
|
193
|
+
# {'saturated_fraction': 0.0052, 'n_intervals': 3, 'total_saturated_sec': 12.9, 'muted': True, ...}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`saturation_mask` gives a boolean array at the reader's own — decimated — sampling rate,
|
|
197
|
+
indexed exactly like the reader itself rather than called with a window:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
n = int(10 * sr.fs)
|
|
201
|
+
traces = sr[:n, :] # (n, nc)
|
|
202
|
+
mask = sr.saturation_mask[:n] # (n,) bool, True where saturated
|
|
203
|
+
|
|
204
|
+
traces_clean = traces[~mask]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
For session-clock timestamps of saturated intervals (e.g. to overlay against trial events),
|
|
208
|
+
use `saturation_times` rather than converting `sr.saturation`'s samples to seconds by hand —
|
|
209
|
+
it applies the same sync correction as [`sr.times`](#session-clock-times):
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
sr.saturation_times()
|
|
213
|
+
# start_sample stop_sample start_time stop_time
|
|
214
|
+
# 0 123456 125000 1234.382 1235.000
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
All four (`saturation`, `saturation_summary`, `saturation_mask`, `saturation_times`) degrade
|
|
218
|
+
gracefully to empty/default values on recordings without saturation detection, so the same
|
|
219
|
+
code works across the whole dataset without a try/except.
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
{.lightbox}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
## Channel brain locations
|
|
226
|
+
|
|
227
|
+
Each recording stores per-channel brain location annotations embedded in the file —
|
|
228
|
+
no network call needed. Access them via `sr.channels`:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from lfpack import LFPackReader
|
|
232
|
+
|
|
233
|
+
sr = LFPackReader("lf_compressed_all_bwm.h5", recording=pid)
|
|
234
|
+
ch = sr.channels
|
|
235
|
+
|
|
236
|
+
print(ch["acronym"][:5]) # ['VISp', 'VISp', 'CA1', 'CA1', 'DG']
|
|
237
|
+
print(ch["atlas_id"][:5]) # Allen CCF structure IDs, int32
|
|
238
|
+
print(ch["x"][:5]) # mediolateral MNI coordinates, metres
|
|
239
|
+
print(ch["y"][:5]) # anteroposterior
|
|
240
|
+
print(ch["z"][:5]) # dorsoventral
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
When using `bin_channels`, `channels` automatically aggregates: coordinates are
|
|
244
|
+
averaged and brain region is the within-group mode:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
sr = LFPackReader("lf_compressed_all_bwm.h5", recording=pid, bin_channels=4)
|
|
248
|
+
ch = sr.channels # shapes (96,) — one entry per spatial bin
|
|
249
|
+
ch_full = sr.channels_full # shapes (384,) — always raw per-electrode
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Binned-channel reads
|
|
253
|
+
|
|
254
|
+
Adjacent channels are spatially correlated at LFP frequencies.
|
|
255
|
+
Passing `bin_channels` sums neighbouring channels during decompression, reducing
|
|
256
|
+
memory use. A factor of 4 takes 384 channels down to 96 while keeping depth resolution useful for LFP.
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
from lfpack import LFPackReader
|
|
260
|
+
|
|
261
|
+
file_lfpack = "lf_compressed_all_bwm.h5"
|
|
262
|
+
pid = 'dab512bd-a02d-4c1f-8dbc-9155a163efc0'
|
|
263
|
+
|
|
264
|
+
sr = LFPackReader(file_lfpack, recording=pid, bin_channels=4)
|
|
265
|
+
|
|
266
|
+
traces = sr[:, :] # (ns, 96) — full recording, 96 spatial bins
|
|
267
|
+
print(sr.nc) # 96
|
|
268
|
+
print(sr.geometry["y"]) # mean depth of each 4-channel group
|
|
269
|
+
print(sr.geometry_full['binned_channel_index']) # spatial mapping of the original 384 channels into the 96 binned ones
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
{fig-alt="Side-by-side LFP density plots at full and 4x binned channel resolution" .lightbox}
|
|
@@ -40,19 +40,4 @@ Higher scale indices correspond to progressively coarser spatial representations
|
|
|
40
40
|
|
|
41
41
|
## Write a multi-recording file
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```python
|
|
46
|
-
from lfpack import compress_to_h5
|
|
47
|
-
import numpy as np
|
|
48
|
-
|
|
49
|
-
# First probe
|
|
50
|
-
data_a = np.load("probe00_lfp.npy") # (nc, ns) float32
|
|
51
|
-
compress_to_h5(data_a, fs=250.0, h5_file="session.lf.h5", recording="probe00")
|
|
52
|
-
|
|
53
|
-
# Second probe — appended to the same file
|
|
54
|
-
data_b = np.load("probe01_lfp.npy")
|
|
55
|
-
compress_to_h5(data_b, fs=250.0, h5_file="session.lf.h5", recording="probe01")
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
See [`compress_to_h5`](../reference/compress_to_h5.qmd) for the full parameter list.
|
|
43
|
+
See [how to write your own lfpack file](write-your-own-file.qmd#write-a-multi-recording-file).
|