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.
Files changed (70) hide show
  1. {lfpack-0.2.0 → lfpack-0.4.0}/.gitignore +1 -0
  2. lfpack-0.4.0/CHANGELOG.md +62 -0
  3. lfpack-0.4.0/CLAUDE.md +97 -0
  4. {lfpack-0.2.0 → lfpack-0.4.0}/PKG-INFO +24 -6
  5. {lfpack-0.2.0 → lfpack-0.4.0}/README.md +22 -4
  6. {lfpack-0.2.0 → lfpack-0.4.0}/docs/_quarto.yml +11 -1
  7. {lfpack-0.2.0 → lfpack-0.4.0}/docs/explanation/pipeline.qmd +3 -1
  8. lfpack-0.4.0/docs/figures/bwm_compression_hf_rolloff.png +0 -0
  9. lfpack-0.4.0/docs/figures/bwm_saturation_detection.png +0 -0
  10. lfpack-0.4.0/docs/figures/viewephys_recording_selector.jpg +0 -0
  11. lfpack-0.4.0/docs/how-to/bwm-dataset.qmd +272 -0
  12. {lfpack-0.2.0 → lfpack-0.4.0}/docs/how-to/multi-recording.qmd +1 -16
  13. lfpack-0.4.0/docs/how-to/write-your-own-file.qmd +108 -0
  14. {lfpack-0.2.0 → lfpack-0.4.0}/docs/index.qmd +1 -0
  15. lfpack-0.4.0/docs/objects.json +1 -0
  16. {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/LFPackReader.qmd +31 -0
  17. {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/_sidebar.yml +4 -0
  18. lfpack-0.4.0/docs/reference/compress.qmd +29 -0
  19. {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/compress_bin_to_h5.qmd +25 -19
  20. lfpack-0.4.0/docs/reference/compress_to_h5.qmd +76 -0
  21. {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/hdf5-layout.qmd +42 -1
  22. {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/index.qmd +10 -1
  23. lfpack-0.4.0/docs/reference/merge_h5.qmd +31 -0
  24. {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/run_cadzow_checkpoint.qmd +2 -0
  25. lfpack-0.4.0/docs/reference/subset_h5.qmd +33 -0
  26. {lfpack-0.2.0 → lfpack-0.4.0}/docs/tutorials/first-compression.qmd +1 -0
  27. {lfpack-0.2.0 → lfpack-0.4.0}/pyproject.toml +1 -1
  28. {lfpack-0.2.0 → lfpack-0.4.0}/src/lfpack/__init__.py +2 -0
  29. {lfpack-0.2.0 → lfpack-0.4.0}/src/lfpack/_core.py +408 -34
  30. lfpack-0.4.0/src/lfpack/_sync.py +124 -0
  31. {lfpack-0.2.0 → lfpack-0.4.0}/tests/test_lfpack.py +411 -3
  32. lfpack-0.2.0/CHANGELOG.md +0 -17
  33. lfpack-0.2.0/CLAUDE.md +0 -77
  34. lfpack-0.2.0/docs/how-to/bwm-dataset.qmd +0 -157
  35. lfpack-0.2.0/docs/objects.json +0 -1
  36. lfpack-0.2.0/docs/reference/compress.qmd +0 -21
  37. lfpack-0.2.0/docs/reference/compress_to_h5.qmd +0 -56
  38. {lfpack-0.2.0 → lfpack-0.4.0}/.githooks/pre-commit +0 -0
  39. {lfpack-0.2.0 → lfpack-0.4.0}/.github/workflows/docs.yml +0 -0
  40. {lfpack-0.2.0 → lfpack-0.4.0}/.github/workflows/publish_to_pypi.yaml +0 -0
  41. {lfpack-0.2.0 → lfpack-0.4.0}/.github/workflows/tests.yml +0 -0
  42. {lfpack-0.2.0 → lfpack-0.4.0}/CONTRIBUTING.md +0 -0
  43. {lfpack-0.2.0 → lfpack-0.4.0}/docs/.gitignore +0 -0
  44. {lfpack-0.2.0 → lfpack-0.4.0}/docs/README.txt +0 -0
  45. {lfpack-0.2.0 → lfpack-0.4.0}/docs/explanation/gallery.qmd +0 -0
  46. {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/density_lfp_dab512bd.png +0 -0
  47. {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/favicon.png +0 -0
  48. {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
  49. {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
  50. {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
  51. {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
  52. {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
  53. {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
  54. {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
  55. {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
  56. {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
  57. {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
  58. {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
  59. {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/gallery/2026-06-15_lfp_compression_aggregate.png +0 -0
  60. {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/lfp_binned_comparison.jpg +0 -0
  61. {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/logo.png +0 -0
  62. {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/viewephys_screenshot.jpg +0 -0
  63. {lfpack-0.2.0 → lfpack-0.4.0}/docs/figures/viewephys_screenshot.png +0 -0
  64. {lfpack-0.2.0 → lfpack-0.4.0}/docs/how-to/binned-reads.qmd +0 -0
  65. {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/LFPCompressed.qmd +0 -0
  66. {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/compress_pipeline.qmd +0 -0
  67. {lfpack-0.2.0 → lfpack-0.4.0}/docs/reference/decompress.qmd +0 -0
  68. {lfpack-0.2.0 → lfpack-0.4.0}/docs/references.bib +0 -0
  69. {lfpack-0.2.0 → lfpack-0.4.0}/tests/__init__.py +0 -0
  70. {lfpack-0.2.0 → lfpack-0.4.0}/uv.lock +0 -0
@@ -1,4 +1,5 @@
1
1
  TODO.md
2
+ .DS_Store
2
3
 
3
4
  # Byte-compiled / optimized / DLL files
4
5
  __pycache__/
@@ -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.4
1
+ Metadata-Version: 2.5
2
2
  Name: lfpack
3
- Version: 0.2.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
- **Parameters:** `epsilon` — SVD noise threshold multiplier (default `150`); `alpha` — WP threshold multiplier (default `28`)
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
 
@@ -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
+ ![Average PSD across compression tiers for a representative recording — high frequencies are increasingly attenuated as compression ratio (CR) rises from cadzow-only to default (119×) to aggressive (254×).](../figures/bwm_compression_hf_rolloff.png){.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
+ ![Recording selector (top right) and raw/CSD checkboxes after opening a multi-recording lfpack file — the dataset info panel confirms the native "Neuropixels LFP (lfpack)" backend](../figures/viewephys_recording_selector.jpg){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
+ ![viewephys opened directly on a BWM lfpack file — brain regions colour the depth axis automatically and the searchable selector switches between the file's 699 recordings](../figures/viewephys_screenshot.jpg){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
+ ![Saturation detection across the 699-recording release: per-recording saturated-fraction distribution (left) and the 15 most-affected recordings (right).](../figures/bwm_saturation_detection.png){.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
+ ![Full-resolution (384 ch) vs binned ×4 (96 ch) LFP density for the same 4-second window](../figures/lfp_binned_comparison.jpg){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
- Use `compress_to_h5` (not `compress_bin_to_h5`) and call it once per recording, passing the same output path each time:
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).