calab 0.1.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.
- calab-0.1.0/.gitignore +6 -0
- calab-0.1.0/LICENSE +21 -0
- calab-0.1.0/PKG-INFO +173 -0
- calab-0.1.0/README.md +145 -0
- calab-0.1.0/pyproject.toml +42 -0
- calab-0.1.0/src/calab/__init__.py +21 -0
- calab-0.1.0/src/calab/_filter.py +103 -0
- calab-0.1.0/src/calab/_fista.py +271 -0
- calab-0.1.0/src/calab/_io.py +238 -0
- calab-0.1.0/src/calab/_kernel.py +113 -0
- calab-0.1.0/src/calab/py.typed +0 -0
- calab-0.1.0/tests/__init__.py +0 -0
- calab-0.1.0/tests/conftest.py +47 -0
- calab-0.1.0/tests/fixtures/README.md +25 -0
- calab-0.1.0/tests/fixtures/fast_kinetics.json +244 -0
- calab-0.1.0/tests/fixtures/high_lambda.json +171 -0
- calab-0.1.0/tests/fixtures/standard_clean.json +174 -0
- calab-0.1.0/tests/fixtures/standard_dc_offset.json +153 -0
- calab-0.1.0/tests/fixtures/with_filter.json +798 -0
- calab-0.1.0/tests/test_cross_language.py +211 -0
- calab-0.1.0/tests/test_equivalence.py +272 -0
- calab-0.1.0/tests/test_filter.py +109 -0
- calab-0.1.0/tests/test_fista.py +326 -0
- calab-0.1.0/tests/test_io.py +331 -0
- calab-0.1.0/tests/test_kernel.py +183 -0
calab-0.1.0/.gitignore
ADDED
calab-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Daniel Aharoni
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
calab-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: calab
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CaLab: calcium imaging analysis tools — deconvolution and data preparation
|
|
5
|
+
Project-URL: Homepage, https://github.com/miniscope/CaLab
|
|
6
|
+
Project-URL: Repository, https://github.com/miniscope/CaLab
|
|
7
|
+
Project-URL: Issues, https://github.com/miniscope/CaLab/issues
|
|
8
|
+
Author: Daniel Aharoni
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: FISTA,calcium-imaging,deconvolution,neuroscience,spike-inference
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: numpy>=1.24
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# CaLab Python
|
|
30
|
+
|
|
31
|
+
Calcium imaging analysis tools — deconvolution and data preparation. Python companion package for the [CaLab](https://github.com/miniscope/CaLab) tools.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install calab
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import calab
|
|
43
|
+
|
|
44
|
+
# Build a calcium kernel
|
|
45
|
+
kernel = calab.build_kernel(tau_rise=0.02, tau_decay=0.4, fs=30.0)
|
|
46
|
+
|
|
47
|
+
# Get AR(2) coefficients
|
|
48
|
+
g1, g2, d, r = calab.tau_to_ar2(tau_rise=0.02, tau_decay=0.4, fs=30.0)
|
|
49
|
+
|
|
50
|
+
# Compute Lipschitz constant for FISTA step size
|
|
51
|
+
L = calab.compute_lipschitz(kernel)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Deconvolution
|
|
55
|
+
|
|
56
|
+
Run FISTA deconvolution matching the CaTune web app's Rust solver exactly
|
|
57
|
+
(baseline estimation + lambda scaling by kernel DC gain):
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import numpy as np
|
|
61
|
+
import calab
|
|
62
|
+
|
|
63
|
+
# Load your calcium traces (n_cells x n_timepoints)
|
|
64
|
+
traces = np.load("my_traces.npy")
|
|
65
|
+
|
|
66
|
+
# Basic: returns non-negative activity array
|
|
67
|
+
activity = calab.run_deconvolution(traces, fs=30.0, tau_r=0.02, tau_d=0.4, lam=0.01)
|
|
68
|
+
|
|
69
|
+
# Full: returns activity, baseline, reconvolution, iterations, converged
|
|
70
|
+
result = calab.run_deconvolution_full(traces, fs=30.0, tau_r=0.02, tau_d=0.4, lam=0.01)
|
|
71
|
+
print(f"Baseline: {result.baseline}, Converged: {result.converged}")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
> **Note:** The deconvolved output represents scaled neural activity, not discrete
|
|
75
|
+
> spikes or firing rates. The signal is scaled by an unknown constant (indicator
|
|
76
|
+
> expression level, optical path, etc.), so absolute values should not be
|
|
77
|
+
> interpreted as spike counts.
|
|
78
|
+
|
|
79
|
+
## Bandpass Filter
|
|
80
|
+
|
|
81
|
+
Apply the same FFT bandpass filter used in the CaTune web app:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
filtered = calab.bandpass_filter(trace, tau_rise=0.02, tau_decay=0.4, fs=100.0)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Using CaTune Export JSON
|
|
88
|
+
|
|
89
|
+
Load parameters from a CaTune export JSON and run deconvolution:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import calab
|
|
93
|
+
|
|
94
|
+
# Load export params
|
|
95
|
+
params = calab.load_export_params("catune-params-2025-01-15.json")
|
|
96
|
+
# -> {'tau_rise': 0.02, 'tau_decay': 0.4, 'lambda_': 0.01, 'fs': 30.0, 'filter_enabled': False}
|
|
97
|
+
|
|
98
|
+
# One-step pipeline: loads params, optionally filters, and deconvolves
|
|
99
|
+
activity = calab.deconvolve_from_export(traces, "catune-params-2025-01-15.json")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Saving Data for CaTune
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import calab
|
|
106
|
+
|
|
107
|
+
calab.save_for_tuning(traces, fs=30.0, path="my_recording")
|
|
108
|
+
# Creates my_recording.npy + my_recording_metadata.json
|
|
109
|
+
# Load into CaTune browser tool via the .npy file
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Converting from CaImAn / Minian
|
|
113
|
+
|
|
114
|
+
CaLab works with raw calcium traces extracted by any pipeline. Use
|
|
115
|
+
`save_for_tuning()` to convert extracted traces into CaTune-compatible
|
|
116
|
+
format. No additional dependencies are required -- users extract arrays
|
|
117
|
+
with their existing pipeline tools.
|
|
118
|
+
|
|
119
|
+
### CaImAn
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
import h5py
|
|
123
|
+
import calab
|
|
124
|
+
|
|
125
|
+
with h5py.File("caiman_results.hdf5", "r") as f:
|
|
126
|
+
traces = f["estimates/C"][:] # shape: (n_cells, n_timepoints)
|
|
127
|
+
fs = float(f["params/data/fr"][()])
|
|
128
|
+
|
|
129
|
+
calab.save_for_tuning(traces, fs, "my_recording")
|
|
130
|
+
# -> my_recording.npy + my_recording_metadata.json, ready for CaTune
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Minian
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import zarr
|
|
137
|
+
import calab
|
|
138
|
+
|
|
139
|
+
store = zarr.open("minian_output", mode="r")
|
|
140
|
+
traces = store["C"][:] # shape: (n_cells, n_frames)
|
|
141
|
+
fs = 30.0 # user must know their frame rate
|
|
142
|
+
|
|
143
|
+
calab.save_for_tuning(traces, fs, "my_recording")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Then deconvolve
|
|
147
|
+
|
|
148
|
+
After tuning parameters in CaTune's browser interface, export your settings
|
|
149
|
+
and apply them in Python:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
import numpy as np
|
|
153
|
+
import calab
|
|
154
|
+
|
|
155
|
+
traces = np.load("my_recording.npy")
|
|
156
|
+
activity = calab.deconvolve_from_export(traces, "catune-params.json")
|
|
157
|
+
# activity is non-negative deconvolved neural activity (scaled by unknown constant)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## API Reference
|
|
161
|
+
|
|
162
|
+
| Function | Description |
|
|
163
|
+
| ------------------------------------------------------- | ----------------------------------------- |
|
|
164
|
+
| `build_kernel(tau_rise, tau_decay, fs)` | Build double-exponential calcium kernel |
|
|
165
|
+
| `tau_to_ar2(tau_rise, tau_decay, fs)` | Derive AR(2) coefficients from tau values |
|
|
166
|
+
| `compute_lipschitz(kernel)` | Lipschitz constant for FISTA step size |
|
|
167
|
+
| `run_deconvolution(traces, fs, tau_r, tau_d, lam)` | FISTA deconvolution, returns activity |
|
|
168
|
+
| `run_deconvolution_full(traces, fs, tau_r, tau_d, lam)` | Full result with baseline, reconvolution |
|
|
169
|
+
| `bandpass_filter(trace, tau_rise, tau_decay, fs)` | FFT bandpass filter from kernel params |
|
|
170
|
+
| `save_for_tuning(traces, fs, path)` | Save traces for CaTune browser tool |
|
|
171
|
+
| `load_tuning_data(path)` | Load traces saved by save_for_tuning |
|
|
172
|
+
| `load_export_params(path)` | Load params from CaTune export JSON |
|
|
173
|
+
| `deconvolve_from_export(traces, params_path)` | Full pipeline: load params + deconvolve |
|
calab-0.1.0/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# CaLab Python
|
|
2
|
+
|
|
3
|
+
Calcium imaging analysis tools — deconvolution and data preparation. Python companion package for the [CaLab](https://github.com/miniscope/CaLab) tools.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install calab
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import calab
|
|
15
|
+
|
|
16
|
+
# Build a calcium kernel
|
|
17
|
+
kernel = calab.build_kernel(tau_rise=0.02, tau_decay=0.4, fs=30.0)
|
|
18
|
+
|
|
19
|
+
# Get AR(2) coefficients
|
|
20
|
+
g1, g2, d, r = calab.tau_to_ar2(tau_rise=0.02, tau_decay=0.4, fs=30.0)
|
|
21
|
+
|
|
22
|
+
# Compute Lipschitz constant for FISTA step size
|
|
23
|
+
L = calab.compute_lipschitz(kernel)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Deconvolution
|
|
27
|
+
|
|
28
|
+
Run FISTA deconvolution matching the CaTune web app's Rust solver exactly
|
|
29
|
+
(baseline estimation + lambda scaling by kernel DC gain):
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import numpy as np
|
|
33
|
+
import calab
|
|
34
|
+
|
|
35
|
+
# Load your calcium traces (n_cells x n_timepoints)
|
|
36
|
+
traces = np.load("my_traces.npy")
|
|
37
|
+
|
|
38
|
+
# Basic: returns non-negative activity array
|
|
39
|
+
activity = calab.run_deconvolution(traces, fs=30.0, tau_r=0.02, tau_d=0.4, lam=0.01)
|
|
40
|
+
|
|
41
|
+
# Full: returns activity, baseline, reconvolution, iterations, converged
|
|
42
|
+
result = calab.run_deconvolution_full(traces, fs=30.0, tau_r=0.02, tau_d=0.4, lam=0.01)
|
|
43
|
+
print(f"Baseline: {result.baseline}, Converged: {result.converged}")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> **Note:** The deconvolved output represents scaled neural activity, not discrete
|
|
47
|
+
> spikes or firing rates. The signal is scaled by an unknown constant (indicator
|
|
48
|
+
> expression level, optical path, etc.), so absolute values should not be
|
|
49
|
+
> interpreted as spike counts.
|
|
50
|
+
|
|
51
|
+
## Bandpass Filter
|
|
52
|
+
|
|
53
|
+
Apply the same FFT bandpass filter used in the CaTune web app:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
filtered = calab.bandpass_filter(trace, tau_rise=0.02, tau_decay=0.4, fs=100.0)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Using CaTune Export JSON
|
|
60
|
+
|
|
61
|
+
Load parameters from a CaTune export JSON and run deconvolution:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import calab
|
|
65
|
+
|
|
66
|
+
# Load export params
|
|
67
|
+
params = calab.load_export_params("catune-params-2025-01-15.json")
|
|
68
|
+
# -> {'tau_rise': 0.02, 'tau_decay': 0.4, 'lambda_': 0.01, 'fs': 30.0, 'filter_enabled': False}
|
|
69
|
+
|
|
70
|
+
# One-step pipeline: loads params, optionally filters, and deconvolves
|
|
71
|
+
activity = calab.deconvolve_from_export(traces, "catune-params-2025-01-15.json")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Saving Data for CaTune
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import calab
|
|
78
|
+
|
|
79
|
+
calab.save_for_tuning(traces, fs=30.0, path="my_recording")
|
|
80
|
+
# Creates my_recording.npy + my_recording_metadata.json
|
|
81
|
+
# Load into CaTune browser tool via the .npy file
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Converting from CaImAn / Minian
|
|
85
|
+
|
|
86
|
+
CaLab works with raw calcium traces extracted by any pipeline. Use
|
|
87
|
+
`save_for_tuning()` to convert extracted traces into CaTune-compatible
|
|
88
|
+
format. No additional dependencies are required -- users extract arrays
|
|
89
|
+
with their existing pipeline tools.
|
|
90
|
+
|
|
91
|
+
### CaImAn
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import h5py
|
|
95
|
+
import calab
|
|
96
|
+
|
|
97
|
+
with h5py.File("caiman_results.hdf5", "r") as f:
|
|
98
|
+
traces = f["estimates/C"][:] # shape: (n_cells, n_timepoints)
|
|
99
|
+
fs = float(f["params/data/fr"][()])
|
|
100
|
+
|
|
101
|
+
calab.save_for_tuning(traces, fs, "my_recording")
|
|
102
|
+
# -> my_recording.npy + my_recording_metadata.json, ready for CaTune
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Minian
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import zarr
|
|
109
|
+
import calab
|
|
110
|
+
|
|
111
|
+
store = zarr.open("minian_output", mode="r")
|
|
112
|
+
traces = store["C"][:] # shape: (n_cells, n_frames)
|
|
113
|
+
fs = 30.0 # user must know their frame rate
|
|
114
|
+
|
|
115
|
+
calab.save_for_tuning(traces, fs, "my_recording")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Then deconvolve
|
|
119
|
+
|
|
120
|
+
After tuning parameters in CaTune's browser interface, export your settings
|
|
121
|
+
and apply them in Python:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import numpy as np
|
|
125
|
+
import calab
|
|
126
|
+
|
|
127
|
+
traces = np.load("my_recording.npy")
|
|
128
|
+
activity = calab.deconvolve_from_export(traces, "catune-params.json")
|
|
129
|
+
# activity is non-negative deconvolved neural activity (scaled by unknown constant)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## API Reference
|
|
133
|
+
|
|
134
|
+
| Function | Description |
|
|
135
|
+
| ------------------------------------------------------- | ----------------------------------------- |
|
|
136
|
+
| `build_kernel(tau_rise, tau_decay, fs)` | Build double-exponential calcium kernel |
|
|
137
|
+
| `tau_to_ar2(tau_rise, tau_decay, fs)` | Derive AR(2) coefficients from tau values |
|
|
138
|
+
| `compute_lipschitz(kernel)` | Lipschitz constant for FISTA step size |
|
|
139
|
+
| `run_deconvolution(traces, fs, tau_r, tau_d, lam)` | FISTA deconvolution, returns activity |
|
|
140
|
+
| `run_deconvolution_full(traces, fs, tau_r, tau_d, lam)` | Full result with baseline, reconvolution |
|
|
141
|
+
| `bandpass_filter(trace, tau_rise, tau_decay, fs)` | FFT bandpass filter from kernel params |
|
|
142
|
+
| `save_for_tuning(traces, fs, path)` | Save traces for CaTune browser tool |
|
|
143
|
+
| `load_tuning_data(path)` | Load traces saved by save_for_tuning |
|
|
144
|
+
| `load_export_params(path)` | Load params from CaTune export JSON |
|
|
145
|
+
| `deconvolve_from_export(traces, params_path)` | Full pipeline: load params + deconvolve |
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "calab"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CaLab: calcium imaging analysis tools — deconvolution and data preparation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Daniel Aharoni" }]
|
|
14
|
+
keywords = ["calcium-imaging", "deconvolution", "neuroscience", "FISTA", "spike-inference"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Medical Science Apps.",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = ["numpy>=1.24"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/miniscope/CaLab"
|
|
32
|
+
Repository = "https://github.com/miniscope/CaLab"
|
|
33
|
+
Issues = "https://github.com/miniscope/CaLab/issues"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = ["pytest>=7.0"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/calab"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""CaLab: calcium imaging analysis tools — deconvolution and data preparation."""
|
|
2
|
+
|
|
3
|
+
from ._kernel import build_kernel, compute_lipschitz, tau_to_ar2
|
|
4
|
+
from ._fista import run_deconvolution, run_deconvolution_full, DeconvolutionResult
|
|
5
|
+
from ._filter import bandpass_filter
|
|
6
|
+
from ._io import load_tuning_data, save_for_tuning, load_export_params, deconvolve_from_export
|
|
7
|
+
|
|
8
|
+
__version__ = "0.2.0"
|
|
9
|
+
__all__ = [
|
|
10
|
+
"build_kernel",
|
|
11
|
+
"tau_to_ar2",
|
|
12
|
+
"compute_lipschitz",
|
|
13
|
+
"run_deconvolution",
|
|
14
|
+
"run_deconvolution_full",
|
|
15
|
+
"DeconvolutionResult",
|
|
16
|
+
"bandpass_filter",
|
|
17
|
+
"save_for_tuning",
|
|
18
|
+
"load_tuning_data",
|
|
19
|
+
"load_export_params",
|
|
20
|
+
"deconvolve_from_export",
|
|
21
|
+
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Bandpass filter -- port of wasm/catune-solver/src/filter.rs.
|
|
2
|
+
|
|
3
|
+
Pure numpy FFT-based bandpass filter derived from kernel time constants.
|
|
4
|
+
Uses cosine-tapered gain curve matching Rust's implementation exactly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
# Margin factors for deriving bandpass cutoffs from kernel time constants.
|
|
12
|
+
# HP cutoff = 1/(2*pi*tau_decay*M_HP), LP cutoff = M_LP/(2*pi*tau_rise).
|
|
13
|
+
# HP uses 16x to preserve the slow calcium decay tail.
|
|
14
|
+
# LP uses 4x for tighter noise rejection above the kernel's rise band.
|
|
15
|
+
_MARGIN_FACTOR_HP = 16.0
|
|
16
|
+
_MARGIN_FACTOR_LP = 4.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def bandpass_filter(
|
|
20
|
+
trace: np.ndarray,
|
|
21
|
+
tau_rise: float,
|
|
22
|
+
tau_decay: float,
|
|
23
|
+
fs: float,
|
|
24
|
+
) -> np.ndarray:
|
|
25
|
+
"""Apply FFT bandpass filter derived from kernel time constants.
|
|
26
|
+
|
|
27
|
+
Cutoffs are computed from time constants (matching filter.rs:62-90):
|
|
28
|
+
- f_hp = 1 / (2*pi * tau_decay * 16)
|
|
29
|
+
- f_lp = min(4 / (2*pi * tau_rise), fs/2)
|
|
30
|
+
|
|
31
|
+
The gain curve uses cosine tapers at transitions (matching filter.rs:130-162).
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
trace : np.ndarray
|
|
36
|
+
1-D calcium trace to filter.
|
|
37
|
+
tau_rise : float
|
|
38
|
+
Rise time constant in seconds.
|
|
39
|
+
tau_decay : float
|
|
40
|
+
Decay time constant in seconds.
|
|
41
|
+
fs : float
|
|
42
|
+
Sampling rate in Hz.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
np.ndarray
|
|
47
|
+
Filtered trace. Returns input unchanged if filter would be invalid
|
|
48
|
+
(f_hp >= f_lp) or trace is too short (< 8 samples).
|
|
49
|
+
"""
|
|
50
|
+
n = len(trace)
|
|
51
|
+
if n < 8:
|
|
52
|
+
return trace.copy()
|
|
53
|
+
|
|
54
|
+
nyquist = fs / 2.0
|
|
55
|
+
|
|
56
|
+
# Compute cutoffs (matching filter.rs:62-90)
|
|
57
|
+
f_hp = 1.0 / (2.0 * np.pi * tau_decay * _MARGIN_FACTOR_HP)
|
|
58
|
+
f_lp = _MARGIN_FACTOR_LP / (2.0 * np.pi * tau_rise)
|
|
59
|
+
|
|
60
|
+
# Clamp LP to Nyquist
|
|
61
|
+
if f_lp > nyquist:
|
|
62
|
+
f_lp = nyquist
|
|
63
|
+
|
|
64
|
+
# Invalid if HP >= LP
|
|
65
|
+
if f_hp >= f_lp:
|
|
66
|
+
return trace.copy()
|
|
67
|
+
|
|
68
|
+
# Build cosine-tapered gain curve (matching filter.rs:130-162)
|
|
69
|
+
spectrum_len = n // 2 + 1
|
|
70
|
+
df = fs / n
|
|
71
|
+
freqs = np.arange(spectrum_len) * df
|
|
72
|
+
|
|
73
|
+
# Taper widths: 50% of respective cutoff frequency
|
|
74
|
+
w_hp = f_hp * 0.5
|
|
75
|
+
w_lp = f_lp * 0.5
|
|
76
|
+
|
|
77
|
+
gain = np.zeros(spectrum_len)
|
|
78
|
+
for i in range(spectrum_len):
|
|
79
|
+
f = freqs[i]
|
|
80
|
+
if f < f_hp - w_hp:
|
|
81
|
+
# Stopband (below high-pass)
|
|
82
|
+
gain[i] = 0.0
|
|
83
|
+
elif f < f_hp + w_hp:
|
|
84
|
+
# HP transition (cosine taper 0 -> 1)
|
|
85
|
+
t = (f - (f_hp - w_hp)) / (2.0 * w_hp)
|
|
86
|
+
gain[i] = 0.5 * (1.0 - np.cos(np.pi * t))
|
|
87
|
+
elif f < f_lp - w_lp:
|
|
88
|
+
# Passband
|
|
89
|
+
gain[i] = 1.0
|
|
90
|
+
elif f < f_lp + w_lp:
|
|
91
|
+
# LP transition (cosine taper 1 -> 0)
|
|
92
|
+
t = (f - (f_lp - w_lp)) / (2.0 * w_lp)
|
|
93
|
+
gain[i] = 0.5 * (1.0 + np.cos(np.pi * t))
|
|
94
|
+
else:
|
|
95
|
+
# Stopband (above low-pass)
|
|
96
|
+
gain[i] = 0.0
|
|
97
|
+
|
|
98
|
+
# Apply via rfft -> multiply -> irfft
|
|
99
|
+
spectrum = np.fft.rfft(trace)
|
|
100
|
+
spectrum *= gain
|
|
101
|
+
filtered = np.fft.irfft(spectrum, n=n)
|
|
102
|
+
|
|
103
|
+
return filtered
|