jnwb 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.
- jnwb-0.1.0/LICENSE +21 -0
- jnwb-0.1.0/PKG-INFO +303 -0
- jnwb-0.1.0/README.md +245 -0
- jnwb-0.1.0/jnwb/__init__.py +433 -0
- jnwb-0.1.0/jnwb/addressing.py +193 -0
- jnwb-0.1.0/jnwb/analyzers.py +750 -0
- jnwb-0.1.0/jnwb/artifact_detection.py +103 -0
- jnwb-0.1.0/jnwb/artifact_repair.py +350 -0
- jnwb-0.1.0/jnwb/bilinear.py +155 -0
- jnwb-0.1.0/jnwb/compression.py +525 -0
- jnwb-0.1.0/jnwb/connectivity.py +2025 -0
- jnwb-0.1.0/jnwb/decoding.py +344 -0
- jnwb-0.1.0/jnwb/gpu_pca.py +95 -0
- jnwb-0.1.0/jnwb/jrsa.py +1552 -0
- jnwb-0.1.0/jnwb/mcp_server/__init__.py +15 -0
- jnwb-0.1.0/jnwb/mcp_server/custom_tools.py +4 -0
- jnwb-0.1.0/jnwb/mcp_server/event_tools.py +148 -0
- jnwb-0.1.0/jnwb/mcp_server/meta_tools.py +76 -0
- jnwb-0.1.0/jnwb/mcp_server/nwb_tools.py +138 -0
- jnwb-0.1.0/jnwb/mcp_server/server.py +3 -0
- jnwb-0.1.0/jnwb/metadata.py +575 -0
- jnwb-0.1.0/jnwb/nam.py +118 -0
- jnwb-0.1.0/jnwb/onset_fitting.py +277 -0
- jnwb-0.1.0/jnwb/ontology.py +394 -0
- jnwb-0.1.0/jnwb/paths.py +239 -0
- jnwb-0.1.0/jnwb/permutation.py +135 -0
- jnwb-0.1.0/jnwb/py.typed +0 -0
- jnwb-0.1.0/jnwb/spectral.py +685 -0
- jnwb-0.1.0/jnwb/spiking.py +258 -0
- jnwb-0.1.0/jnwb/statistics.py +1036 -0
- jnwb-0.1.0/jnwb/tfr.py +224 -0
- jnwb-0.1.0/jnwb/tfr_accumulator.py +122 -0
- jnwb-0.1.0/jnwb/trajectory.py +191 -0
- jnwb-0.1.0/jnwb/visual_qc.py +350 -0
- jnwb-0.1.0/jnwb/viz.py +136 -0
- jnwb-0.1.0/jnwb.egg-info/PKG-INFO +303 -0
- jnwb-0.1.0/jnwb.egg-info/SOURCES.txt +105 -0
- jnwb-0.1.0/jnwb.egg-info/dependency_links.txt +1 -0
- jnwb-0.1.0/jnwb.egg-info/requires.txt +36 -0
- jnwb-0.1.0/jnwb.egg-info/top_level.txt +1 -0
- jnwb-0.1.0/pyproject.toml +106 -0
- jnwb-0.1.0/setup.cfg +4 -0
- jnwb-0.1.0/tests/test_addressing.py +138 -0
- jnwb-0.1.0/tests/test_analyzers_coverage.py +350 -0
- jnwb-0.1.0/tests/test_artifact_detection.py +112 -0
- jnwb-0.1.0/tests/test_artifact_repair.py +131 -0
- jnwb-0.1.0/tests/test_audit_reproducers.py +53 -0
- jnwb-0.1.0/tests/test_batch_a_regressions.py +289 -0
- jnwb-0.1.0/tests/test_complex_tfr.py +301 -0
- jnwb-0.1.0/tests/test_compression.py +83 -0
- jnwb-0.1.0/tests/test_connectivity.py +174 -0
- jnwb-0.1.0/tests/test_decoding.py +193 -0
- jnwb-0.1.0/tests/test_docs_smoke.py +157 -0
- jnwb-0.1.0/tests/test_gpu_pca.py +49 -0
- jnwb-0.1.0/tests/test_harness_adversarial_gates.py +93 -0
- jnwb-0.1.0/tests/test_jnwb_core.py +295 -0
- jnwb-0.1.0/tests/test_jnwb_frozen_boundary.py +116 -0
- jnwb-0.1.0/tests/test_jrsa_correctness.py +47 -0
- jnwb-0.1.0/tests/test_jrsa_gpu.py +60 -0
- jnwb-0.1.0/tests/test_jrsa_no_fabricated_failures.py +68 -0
- jnwb-0.1.0/tests/test_mcp_server.py +161 -0
- jnwb-0.1.0/tests/test_metadata.py +280 -0
- jnwb-0.1.0/tests/test_onset_fitting.py +105 -0
- jnwb-0.1.0/tests/test_paths.py +83 -0
- jnwb-0.1.0/tests/test_permutation.py +156 -0
- jnwb-0.1.0/tests/test_representative_workflow.py +213 -0
- jnwb-0.1.0/tests/test_spectral.py +194 -0
- jnwb-0.1.0/tests/test_spiking.py +87 -0
- jnwb-0.1.0/tests/test_statistics.py +296 -0
- jnwb-0.1.0/tests/test_statistics_api_split.py +176 -0
- jnwb-0.1.0/tests/test_tfr_accumulator.py +244 -0
- jnwb-0.1.0/tests/test_tfr_extract_band_regressions.py +170 -0
- jnwb-0.1.0/tests/test_trajectory.py +119 -0
- jnwb-0.1.0/tests/test_visual_qc.py +97 -0
- jnwb-0.1.0/tests/test_viz.py +126 -0
jnwb-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hamed Nejat
|
|
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.
|
jnwb-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jnwb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generic Python library for high-density electrophysiology, time-frequency analysis, and statistical neuroscience on Neurodata Without Borders (NWB) datasets.
|
|
5
|
+
Author: Hamed Nejat
|
|
6
|
+
Maintainer: Hamed Nejat
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/HNXJ/jnwb
|
|
9
|
+
Project-URL: Repository, https://github.com/HNXJ/jnwb.git
|
|
10
|
+
Project-URL: Documentation, https://github.com/HNXJ/jnwb#readme
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/HNXJ/jnwb/issues
|
|
12
|
+
Keywords: neuroscience,electrophysiology,nwb,neurodata-without-borders,lfp,spikes,time-frequency,morlet-wavelet,granger-causality,phase-slope-index
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: numpy>=1.22.0
|
|
28
|
+
Requires-Dist: scipy>=1.8.0
|
|
29
|
+
Requires-Dist: pandas>=1.4.0
|
|
30
|
+
Requires-Dist: h5py>=3.6.0
|
|
31
|
+
Requires-Dist: pynwb>=2.0.0
|
|
32
|
+
Requires-Dist: hdmf>=3.1.0
|
|
33
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
34
|
+
Requires-Dist: scikit-learn>=1.0.0
|
|
35
|
+
Requires-Dist: statsmodels>=0.13.0
|
|
36
|
+
Requires-Dist: joblib>=1.1.0
|
|
37
|
+
Provides-Extra: mcp
|
|
38
|
+
Requires-Dist: mcp<2.0; extra == "mcp"
|
|
39
|
+
Provides-Extra: torch
|
|
40
|
+
Requires-Dist: torch>=1.12.0; extra == "torch"
|
|
41
|
+
Provides-Extra: gpu
|
|
42
|
+
Requires-Dist: cupy-cuda12x; extra == "gpu"
|
|
43
|
+
Requires-Dist: jax; extra == "gpu"
|
|
44
|
+
Provides-Extra: test
|
|
45
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
46
|
+
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
47
|
+
Requires-Dist: pytest-xdist>=3.0; extra == "test"
|
|
48
|
+
Requires-Dist: build>=1.0.0; extra == "test"
|
|
49
|
+
Requires-Dist: twine>=4.0.0; extra == "test"
|
|
50
|
+
Requires-Dist: mcp<2.0; extra == "test"
|
|
51
|
+
Provides-Extra: docs
|
|
52
|
+
Requires-Dist: sphinx>=6.0.0; extra == "docs"
|
|
53
|
+
Requires-Dist: sphinx-rtd-theme>=1.2.0; extra == "docs"
|
|
54
|
+
Requires-Dist: myst-parser>=1.0.0; extra == "docs"
|
|
55
|
+
Provides-Extra: all
|
|
56
|
+
Requires-Dist: jnwb[docs,gpu,mcp,test,torch]; extra == "all"
|
|
57
|
+
Dynamic: license-file
|
|
58
|
+
|
|
59
|
+
# `jnwb`
|
|
60
|
+
|
|
61
|
+
Dataset-agnostic Python library for NWB (Neurodata Without Borders) electrophysiology analysis:
|
|
62
|
+
session I/O, addressing (channel→area, depth→layer), representational similarity analysis (JRSA),
|
|
63
|
+
TFR accumulation/compression, generic statistics, and visual QC.
|
|
64
|
+
|
|
65
|
+
`jnwb` makes no assumptions about task structure, condition codes, or experiment design. For a
|
|
66
|
+
worked example of building a full project on top of it — including a task-specific extension
|
|
67
|
+
package, scripts, notebooks, and a manuscript pipeline — see [`omission/`](omission/README.md),
|
|
68
|
+
this repo's native large-dataset example project.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install -e ".[test]"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Paths — do this first after any drive remap
|
|
79
|
+
|
|
80
|
+
Repo-internal paths (`REPO_ROOT`, `outputs_dir()`, `artifacts_dir()`) resolve from the package's
|
|
81
|
+
own location and always work. External data roots live on a separate volume and are set by
|
|
82
|
+
environment variable — see [`jnwb/paths.py`](jnwb/paths.py) for the full list and defaults.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import jnwb
|
|
86
|
+
jnwb.paths.describe() # every root + whether it currently resolves
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If a root shows `exists: false`, set its env var — do not edit source, and do not write a new
|
|
90
|
+
absolute literal into a script.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Quick start
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import jnwb
|
|
98
|
+
|
|
99
|
+
result = jnwb.jrsa(x1, x2, metric='rsa', stats=True)
|
|
100
|
+
result.summary()
|
|
101
|
+
result.plot()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Null construction under an explicit exchangeability scheme — every call must name `scheme`
|
|
105
|
+
(`"within_group"` or `"global"`), there is no default, since a bare `rng.permutation(y)` inside
|
|
106
|
+
grouped/session-structured decoding is a documented past bug (see `jnwb/permutation.py`):
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import numpy as np
|
|
110
|
+
rng = np.random.default_rng(0)
|
|
111
|
+
null_labels = jnwb.permute_labels(labels, groups=cycle_id, scheme="within_group", rng=rng)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Trial-segmented LFP/TFR artifact detection-and-substitution (cross-channel-synchrony detection,
|
|
115
|
+
cross-trial-median repair; see `jnwb/artifact_repair.py`):
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
repaired, frac_flagged, diagnostics = jnwb.repair_lfp_trials(
|
|
119
|
+
segments, times_ms=times_ms, z_thresh=6.0) # segments: (n_trials, n_channels, n_times)
|
|
120
|
+
repaired_power, frac_flagged_by_band = jnwb.repair_band_artifacts(power, freqs) # per-band TFR
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Causal PSTH smoothing and causality-bounded exponential onset-latency fit (a forward-only
|
|
124
|
+
kernel by design — an acausal/centered smoother would let post-onset activity bias the fitted
|
|
125
|
+
onset earlier than the true rise; see `jnwb/onset_fitting.py`):
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
smoothed = jnwb.causal_exp_smooth(rate, bin_ms=5.0, tau_ms=30.0)
|
|
129
|
+
fit = jnwb.fit_exponential_onset(t_ms, smoothed, t0_bounds=(0.0, None)) # fit["t0"], ["tau"], ["r2"]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Unit/electrode metadata extraction, QC classification, and census reporting, from a plain list
|
|
133
|
+
of NWB paths:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
units = jnwb.get_all_units_metadata(nwb_paths) # -> DataFrame, one row per unit
|
|
137
|
+
units = jnwb.classify_unit_quality(units) # + quality_class, is_valid, issue_flags
|
|
138
|
+
census = jnwb.unit_census_report(units, group_by=["area"])
|
|
139
|
+
snr_stats = jnwb.get_snr_analysis(units) # -> {'pass_rate': ..., 'snr_mean': ...}
|
|
140
|
+
good_v1 = jnwb.filter_by_criteria(units, {"area": "V1", "firing_rate": (1.0, 50.0)})
|
|
141
|
+
unit_audit = jnwb.audit_units(units) # spike-time coverage, quality/SNR/rate stats
|
|
142
|
+
elec_audit = jnwb.audit_electrodes(electrodes, units) # area counts, unit-assignment rate
|
|
143
|
+
tier = jnwb.assign_quality_tier(units["quality"], units["trial_presence_fraction"], units["snr"])
|
|
144
|
+
diff = jnwb.compare_old_new_criteria(new_units, old_units) # gained/lost/unchanged transitions
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Generic paired fire-probability testing — plain spike-time/onset arrays and boolean pairs in,
|
|
148
|
+
a shuffle-null p-value + bootstrap CI + odds ratio out (`jnwb/statistics.py`):
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
fired = jnwb.fire_indicator(spike_times, onsets, window_ms=(0.0, 150.0))
|
|
152
|
+
result = jnwb.paired_fire_prob_test(fired_target, fired_baseline, n_shuffles=2000, n_bootstrap=2000, rng=rng)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Generic spike-rate windowing, shuffle-controlled paired/unpaired p-values, temporal
|
|
156
|
+
cycle/quantile detection on a trial table, and a shuffle-null R² CI (`jnwb/statistics.py`):
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
rate_hz = jnwb.rate_in_window(spike_times, onset_s, window_ms=(0.0, 200.0))
|
|
160
|
+
obs, p = jnwb.shuffle_pvalue_paired(a, b, n_shuffles=2000, rng=rng, alternative="greater")
|
|
161
|
+
cycle_id = jnwb.detect_trial_cycles(trials, gap_factor=10.0) # trials: DataFrame with start_time
|
|
162
|
+
r2 = jnwb.shuffle_r2_ci(y_true, y_score, groups=cycle_id, n_shuffle=500)
|
|
163
|
+
result = jnwb.cross_modal_comparison(tfr_data, spike_data) # trial-averaged zero-lag correlation
|
|
164
|
+
result = jnwb.cross_modal_comparison(tfr_data, spike_data, lag_range_ms=(-200, 200), bin_ms=10.0)
|
|
165
|
+
# ^ with bin_ms given, searches lag_range_ms for the best-correlating shift instead of zero-lag only
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Generic spectral analysis — band-limited power, cross-area coherence, 1/f tilt, imaginary
|
|
169
|
+
coherency (immune to zero-lag volume-conduction mixing by construction), and bipolar/Laplacian
|
|
170
|
+
re-referencing (`jnwb/spectral.py`; `CANONICAL_BANDS` is the single-source-of-truth band-edge
|
|
171
|
+
default, theta/alpha/beta/low_gamma/high_gamma):
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
coh = jnwb.cross_area_coherence(v1_lfp, pfc_lfp, sampling_rate=1000.0)
|
|
175
|
+
theta_power_db = jnwb.band_power(lfp, 1000.0, jnwb.CANONICAL_BANDS["theta"], baseline=baseline_lfp)
|
|
176
|
+
icoh = jnwb.imaginary_coherency(x, y, sampling_rate=1000.0, freq_range=(1, 100)) # icoh_mean, coh_mag_mean
|
|
177
|
+
laplacian = jnwb.laplacian_reference(channel_data, channel_order=depth_order)
|
|
178
|
+
freqs, psd = jnwb.compute_psd(lfp_data, fs=1000.0) # plain Welch PSD wrapper
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Modality-agnostic directed functional connectivity — Granger causality, spectral (Geweke)
|
|
182
|
+
Granger, phase slope index, and transfer entropy all share one `(X, Y, ...)` contract and
|
|
183
|
+
return the same `DirectedResult` shape, so LFP traces, binned spike counts, MUAe envelopes, and
|
|
184
|
+
band-power time courses go through identical code (`jnwb/connectivity.py`):
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
rate_a = jnwb.bin_spikes(spike_times_a, window=(-0.5, 1.0), bin_size_ms=10.0, output="rate")
|
|
188
|
+
result = jnwb.granger(v1_lfp, pfc_lfp, order="auto") # or jnwb.phase_slope_index(..., fs=1000.0)
|
|
189
|
+
print(result.x_to_y, result.y_to_x, result.net) # DirectedResult: uniform across estimators
|
|
190
|
+
network = jnwb.directed_network({"V1": v1_lfp, "PFC": pfc_lfp}, method="granger")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Generic nested cross-validated linear-SVM population decoding — plain `(n_trials, n_features)`
|
|
194
|
+
matrix and integer labels in, accuracy/F1/AUC/majority-baseline out, NaN (never fabricated)
|
|
195
|
+
under degenerate class counts (`jnwb/decoding.py`):
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
result = jnwb.nested_cv_linear_svm(X, labels, n_splits=5)
|
|
199
|
+
print(result["accuracy"], result["f1"], result["auc"], result["majority_baseline_accuracy"])
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Generic grouped leave-one-group-out CV geometry, representation contracts (R0/R1/R2), and a
|
|
203
|
+
reproducible within-group null-permutation plan — plain trial `DataFrame`/array in, no session
|
|
204
|
+
or condition semantics (`jnwb/decoding.py`, `jnwb/permutation.py`):
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
outer = jnwb.assign_outer_folds(trials, analysis_cols=("session", "analysis", "slot_key"), group_col="cycle")
|
|
208
|
+
inner = jnwb.build_inner_validation_partitions(outer)
|
|
209
|
+
ladder = jnwb.build_representation_ladder(raster, modality="SPK") # X_rate, X_vec, X_structured
|
|
210
|
+
plan = jnwb.build_permutation_plan(labels, groups, n_permutations=1000, seed=0) # draw_manifest
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Generic spike-response metrics — firing rate/latency/z-score relative to any behavioral epoch,
|
|
214
|
+
significance classification, and spike-LFP phase locking (`jnwb/spiking.py`):
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
metrics = jnwb.compute_response_metrics(spike_times, epoch_onsets, response_window=(0.0, 0.15))
|
|
218
|
+
sig = jnwb.classify_response_significance(metrics) # is_significant, pvalue, confidence
|
|
219
|
+
pli = jnwb.phase_locking_index(spike_times, lfp_phase, lfp_timestamps) # pli, rayleigh_pvalue
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Generic plotting utilities — vector-graphics setup, tight auto-scaled axes, multi-page/format
|
|
223
|
+
figure export, trial-onset resampling, and array-in PSTH (`jnwb/viz.py`):
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
jnwb.setup_vector_graphics() # editable SVG fonts
|
|
227
|
+
centers, mean_hz, sem_hz = jnwb.raster_psth(spike_times, onsets, win_ms=(-500, 1000))
|
|
228
|
+
jnwb.save_figure_suite(figures, "outputs/figures", basename="raster", formats=["png", "pdf"])
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Module map
|
|
234
|
+
|
|
235
|
+
The public surface is `jnwb/__init__.py` (`__all__`).
|
|
236
|
+
|
|
237
|
+
| Module | Role |
|
|
238
|
+
|---|---|
|
|
239
|
+
| `paths.py` | Repo and data root resolution; the only place absolute paths live |
|
|
240
|
+
| `addressing.py` | Peak-channel → area mapping, depth → layer classification |
|
|
241
|
+
| `ontology.py`, `jrsa.py` | `Dataset`/`AlignedDataset`/`Question`/`Result` objects; unified RSA engine |
|
|
242
|
+
| `statistics.py`, `analyzers.py` | `StatisticalAnalysis`, `TFRAnalyzer`, `UnitAnalyzer`, `PopulationAnalyzer` |
|
|
243
|
+
| `tfr_accumulator.py`, `compression.py` | Poolable TFR summary statistics; NWB fp32 compression |
|
|
244
|
+
| `trajectory.py`, `gpu_pca.py` | Population trajectories via GPU SVD |
|
|
245
|
+
| `visual_qc.py` | Generic visual QC plotting |
|
|
246
|
+
| `bilinear.py`, `nam.py`, `permutation.py` | Generic modeling/statistical primitives |
|
|
247
|
+
| `artifact_repair.py`, `artifact_detection.py` | Trial-segmented artifact repair (substitution) / detection (exclusion) |
|
|
248
|
+
| `onset_fitting.py` | Causal PSTH smoothing; causality-bounded exponential onset-latency fit |
|
|
249
|
+
| `metadata.py` | Unit/electrode metadata extraction, QC classification, census reporting |
|
|
250
|
+
| `spectral.py` | Band power, cross-area coherence, 1/f tilt, imaginary coherency, bipolar/Laplacian re-referencing; `CANONICAL_BANDS` |
|
|
251
|
+
| `connectivity.py` | Mutual information, Granger causality, phase slope index, transfer entropy; uniform `DirectedResult` |
|
|
252
|
+
| `decoding.py` | Nested cross-validated linear-SVM population decoding (accuracy/F1/AUC/majority-baseline) |
|
|
253
|
+
| `spiking.py` | Spike-response firing rate/latency/z-score, significance classification, spike-LFP phase locking |
|
|
254
|
+
| `viz.py` | Vector-graphics setup, tight auto-scaled axes, multi-page/format figure export, onset resampling, array-in PSTH |
|
|
255
|
+
| `mcp_server/` | stdio MCP server: `inspect_nwb`, `get_event_codes_and_timings`, `prepare_signal_reference`, `add_tool` |
|
|
256
|
+
|
|
257
|
+
Task-specific functionality (condition codes, unit classification, decoding, connectivity,
|
|
258
|
+
figure suites) lives in [`omission/jnwb_ext/`](omission/README.md), not here.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## MCP server
|
|
263
|
+
|
|
264
|
+
`jnwb` includes a stdio Model Context Protocol server for NWB inspection from Claude and other
|
|
265
|
+
MCP-compatible clients: `inspect_nwb`, `get_event_codes_and_timings`, `prepare_signal_reference`,
|
|
266
|
+
`add_tool`. Depends on `mcp`, `h5py`, `pynwb`, `pandas`, `numpy` (installed via `pip install -e .`).
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
python -m jnwb.mcp_server
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{
|
|
274
|
+
"mcpServers": {
|
|
275
|
+
"jnwb-mcp-server": {
|
|
276
|
+
"command": "python",
|
|
277
|
+
"args": ["-m", "jnwb.mcp_server"]
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Repository layout
|
|
286
|
+
|
|
287
|
+
| Path | Contents |
|
|
288
|
+
|---|---|
|
|
289
|
+
| `jnwb/` | The library (above) |
|
|
290
|
+
| `tests/` | Pytest suite for the generic library — run it, don't trust pass counts in docs |
|
|
291
|
+
| `omission/` | The example project built on `jnwb` — see [`omission/README.md`](omission/README.md) |
|
|
292
|
+
| `.claude/skills/` | Task-scoped API guides |
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Before you change anything
|
|
297
|
+
|
|
298
|
+
- **`CLAUDE.md`** — repo doctrine: library invariants, footguns, verification checks that caught
|
|
299
|
+
real errors.
|
|
300
|
+
- **`omission/.claude/skills/`** — task-scoped API guides (`omission-data`, `omission-signal`,
|
|
301
|
+
`omission-spiking`, `omission-statistics`, `omission-figures`, `manuscript`, `labyrinth`). There
|
|
302
|
+
is no repo-root `.claude/skills/` — `jnwb/` itself has no dedicated skill yet (see
|
|
303
|
+
`numerical-computing` / `biophysical-modeling`, which are general-purpose, not jnwb-specific).
|
jnwb-0.1.0/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# `jnwb`
|
|
2
|
+
|
|
3
|
+
Dataset-agnostic Python library for NWB (Neurodata Without Borders) electrophysiology analysis:
|
|
4
|
+
session I/O, addressing (channel→area, depth→layer), representational similarity analysis (JRSA),
|
|
5
|
+
TFR accumulation/compression, generic statistics, and visual QC.
|
|
6
|
+
|
|
7
|
+
`jnwb` makes no assumptions about task structure, condition codes, or experiment design. For a
|
|
8
|
+
worked example of building a full project on top of it — including a task-specific extension
|
|
9
|
+
package, scripts, notebooks, and a manuscript pipeline — see [`omission/`](omission/README.md),
|
|
10
|
+
this repo's native large-dataset example project.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install -e ".[test]"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Paths — do this first after any drive remap
|
|
21
|
+
|
|
22
|
+
Repo-internal paths (`REPO_ROOT`, `outputs_dir()`, `artifacts_dir()`) resolve from the package's
|
|
23
|
+
own location and always work. External data roots live on a separate volume and are set by
|
|
24
|
+
environment variable — see [`jnwb/paths.py`](jnwb/paths.py) for the full list and defaults.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import jnwb
|
|
28
|
+
jnwb.paths.describe() # every root + whether it currently resolves
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If a root shows `exists: false`, set its env var — do not edit source, and do not write a new
|
|
32
|
+
absolute literal into a script.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import jnwb
|
|
40
|
+
|
|
41
|
+
result = jnwb.jrsa(x1, x2, metric='rsa', stats=True)
|
|
42
|
+
result.summary()
|
|
43
|
+
result.plot()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Null construction under an explicit exchangeability scheme — every call must name `scheme`
|
|
47
|
+
(`"within_group"` or `"global"`), there is no default, since a bare `rng.permutation(y)` inside
|
|
48
|
+
grouped/session-structured decoding is a documented past bug (see `jnwb/permutation.py`):
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import numpy as np
|
|
52
|
+
rng = np.random.default_rng(0)
|
|
53
|
+
null_labels = jnwb.permute_labels(labels, groups=cycle_id, scheme="within_group", rng=rng)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Trial-segmented LFP/TFR artifact detection-and-substitution (cross-channel-synchrony detection,
|
|
57
|
+
cross-trial-median repair; see `jnwb/artifact_repair.py`):
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
repaired, frac_flagged, diagnostics = jnwb.repair_lfp_trials(
|
|
61
|
+
segments, times_ms=times_ms, z_thresh=6.0) # segments: (n_trials, n_channels, n_times)
|
|
62
|
+
repaired_power, frac_flagged_by_band = jnwb.repair_band_artifacts(power, freqs) # per-band TFR
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Causal PSTH smoothing and causality-bounded exponential onset-latency fit (a forward-only
|
|
66
|
+
kernel by design — an acausal/centered smoother would let post-onset activity bias the fitted
|
|
67
|
+
onset earlier than the true rise; see `jnwb/onset_fitting.py`):
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
smoothed = jnwb.causal_exp_smooth(rate, bin_ms=5.0, tau_ms=30.0)
|
|
71
|
+
fit = jnwb.fit_exponential_onset(t_ms, smoothed, t0_bounds=(0.0, None)) # fit["t0"], ["tau"], ["r2"]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Unit/electrode metadata extraction, QC classification, and census reporting, from a plain list
|
|
75
|
+
of NWB paths:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
units = jnwb.get_all_units_metadata(nwb_paths) # -> DataFrame, one row per unit
|
|
79
|
+
units = jnwb.classify_unit_quality(units) # + quality_class, is_valid, issue_flags
|
|
80
|
+
census = jnwb.unit_census_report(units, group_by=["area"])
|
|
81
|
+
snr_stats = jnwb.get_snr_analysis(units) # -> {'pass_rate': ..., 'snr_mean': ...}
|
|
82
|
+
good_v1 = jnwb.filter_by_criteria(units, {"area": "V1", "firing_rate": (1.0, 50.0)})
|
|
83
|
+
unit_audit = jnwb.audit_units(units) # spike-time coverage, quality/SNR/rate stats
|
|
84
|
+
elec_audit = jnwb.audit_electrodes(electrodes, units) # area counts, unit-assignment rate
|
|
85
|
+
tier = jnwb.assign_quality_tier(units["quality"], units["trial_presence_fraction"], units["snr"])
|
|
86
|
+
diff = jnwb.compare_old_new_criteria(new_units, old_units) # gained/lost/unchanged transitions
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Generic paired fire-probability testing — plain spike-time/onset arrays and boolean pairs in,
|
|
90
|
+
a shuffle-null p-value + bootstrap CI + odds ratio out (`jnwb/statistics.py`):
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
fired = jnwb.fire_indicator(spike_times, onsets, window_ms=(0.0, 150.0))
|
|
94
|
+
result = jnwb.paired_fire_prob_test(fired_target, fired_baseline, n_shuffles=2000, n_bootstrap=2000, rng=rng)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Generic spike-rate windowing, shuffle-controlled paired/unpaired p-values, temporal
|
|
98
|
+
cycle/quantile detection on a trial table, and a shuffle-null R² CI (`jnwb/statistics.py`):
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
rate_hz = jnwb.rate_in_window(spike_times, onset_s, window_ms=(0.0, 200.0))
|
|
102
|
+
obs, p = jnwb.shuffle_pvalue_paired(a, b, n_shuffles=2000, rng=rng, alternative="greater")
|
|
103
|
+
cycle_id = jnwb.detect_trial_cycles(trials, gap_factor=10.0) # trials: DataFrame with start_time
|
|
104
|
+
r2 = jnwb.shuffle_r2_ci(y_true, y_score, groups=cycle_id, n_shuffle=500)
|
|
105
|
+
result = jnwb.cross_modal_comparison(tfr_data, spike_data) # trial-averaged zero-lag correlation
|
|
106
|
+
result = jnwb.cross_modal_comparison(tfr_data, spike_data, lag_range_ms=(-200, 200), bin_ms=10.0)
|
|
107
|
+
# ^ with bin_ms given, searches lag_range_ms for the best-correlating shift instead of zero-lag only
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Generic spectral analysis — band-limited power, cross-area coherence, 1/f tilt, imaginary
|
|
111
|
+
coherency (immune to zero-lag volume-conduction mixing by construction), and bipolar/Laplacian
|
|
112
|
+
re-referencing (`jnwb/spectral.py`; `CANONICAL_BANDS` is the single-source-of-truth band-edge
|
|
113
|
+
default, theta/alpha/beta/low_gamma/high_gamma):
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
coh = jnwb.cross_area_coherence(v1_lfp, pfc_lfp, sampling_rate=1000.0)
|
|
117
|
+
theta_power_db = jnwb.band_power(lfp, 1000.0, jnwb.CANONICAL_BANDS["theta"], baseline=baseline_lfp)
|
|
118
|
+
icoh = jnwb.imaginary_coherency(x, y, sampling_rate=1000.0, freq_range=(1, 100)) # icoh_mean, coh_mag_mean
|
|
119
|
+
laplacian = jnwb.laplacian_reference(channel_data, channel_order=depth_order)
|
|
120
|
+
freqs, psd = jnwb.compute_psd(lfp_data, fs=1000.0) # plain Welch PSD wrapper
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Modality-agnostic directed functional connectivity — Granger causality, spectral (Geweke)
|
|
124
|
+
Granger, phase slope index, and transfer entropy all share one `(X, Y, ...)` contract and
|
|
125
|
+
return the same `DirectedResult` shape, so LFP traces, binned spike counts, MUAe envelopes, and
|
|
126
|
+
band-power time courses go through identical code (`jnwb/connectivity.py`):
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
rate_a = jnwb.bin_spikes(spike_times_a, window=(-0.5, 1.0), bin_size_ms=10.0, output="rate")
|
|
130
|
+
result = jnwb.granger(v1_lfp, pfc_lfp, order="auto") # or jnwb.phase_slope_index(..., fs=1000.0)
|
|
131
|
+
print(result.x_to_y, result.y_to_x, result.net) # DirectedResult: uniform across estimators
|
|
132
|
+
network = jnwb.directed_network({"V1": v1_lfp, "PFC": pfc_lfp}, method="granger")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Generic nested cross-validated linear-SVM population decoding — plain `(n_trials, n_features)`
|
|
136
|
+
matrix and integer labels in, accuracy/F1/AUC/majority-baseline out, NaN (never fabricated)
|
|
137
|
+
under degenerate class counts (`jnwb/decoding.py`):
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
result = jnwb.nested_cv_linear_svm(X, labels, n_splits=5)
|
|
141
|
+
print(result["accuracy"], result["f1"], result["auc"], result["majority_baseline_accuracy"])
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Generic grouped leave-one-group-out CV geometry, representation contracts (R0/R1/R2), and a
|
|
145
|
+
reproducible within-group null-permutation plan — plain trial `DataFrame`/array in, no session
|
|
146
|
+
or condition semantics (`jnwb/decoding.py`, `jnwb/permutation.py`):
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
outer = jnwb.assign_outer_folds(trials, analysis_cols=("session", "analysis", "slot_key"), group_col="cycle")
|
|
150
|
+
inner = jnwb.build_inner_validation_partitions(outer)
|
|
151
|
+
ladder = jnwb.build_representation_ladder(raster, modality="SPK") # X_rate, X_vec, X_structured
|
|
152
|
+
plan = jnwb.build_permutation_plan(labels, groups, n_permutations=1000, seed=0) # draw_manifest
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Generic spike-response metrics — firing rate/latency/z-score relative to any behavioral epoch,
|
|
156
|
+
significance classification, and spike-LFP phase locking (`jnwb/spiking.py`):
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
metrics = jnwb.compute_response_metrics(spike_times, epoch_onsets, response_window=(0.0, 0.15))
|
|
160
|
+
sig = jnwb.classify_response_significance(metrics) # is_significant, pvalue, confidence
|
|
161
|
+
pli = jnwb.phase_locking_index(spike_times, lfp_phase, lfp_timestamps) # pli, rayleigh_pvalue
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Generic plotting utilities — vector-graphics setup, tight auto-scaled axes, multi-page/format
|
|
165
|
+
figure export, trial-onset resampling, and array-in PSTH (`jnwb/viz.py`):
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
jnwb.setup_vector_graphics() # editable SVG fonts
|
|
169
|
+
centers, mean_hz, sem_hz = jnwb.raster_psth(spike_times, onsets, win_ms=(-500, 1000))
|
|
170
|
+
jnwb.save_figure_suite(figures, "outputs/figures", basename="raster", formats=["png", "pdf"])
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Module map
|
|
176
|
+
|
|
177
|
+
The public surface is `jnwb/__init__.py` (`__all__`).
|
|
178
|
+
|
|
179
|
+
| Module | Role |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `paths.py` | Repo and data root resolution; the only place absolute paths live |
|
|
182
|
+
| `addressing.py` | Peak-channel → area mapping, depth → layer classification |
|
|
183
|
+
| `ontology.py`, `jrsa.py` | `Dataset`/`AlignedDataset`/`Question`/`Result` objects; unified RSA engine |
|
|
184
|
+
| `statistics.py`, `analyzers.py` | `StatisticalAnalysis`, `TFRAnalyzer`, `UnitAnalyzer`, `PopulationAnalyzer` |
|
|
185
|
+
| `tfr_accumulator.py`, `compression.py` | Poolable TFR summary statistics; NWB fp32 compression |
|
|
186
|
+
| `trajectory.py`, `gpu_pca.py` | Population trajectories via GPU SVD |
|
|
187
|
+
| `visual_qc.py` | Generic visual QC plotting |
|
|
188
|
+
| `bilinear.py`, `nam.py`, `permutation.py` | Generic modeling/statistical primitives |
|
|
189
|
+
| `artifact_repair.py`, `artifact_detection.py` | Trial-segmented artifact repair (substitution) / detection (exclusion) |
|
|
190
|
+
| `onset_fitting.py` | Causal PSTH smoothing; causality-bounded exponential onset-latency fit |
|
|
191
|
+
| `metadata.py` | Unit/electrode metadata extraction, QC classification, census reporting |
|
|
192
|
+
| `spectral.py` | Band power, cross-area coherence, 1/f tilt, imaginary coherency, bipolar/Laplacian re-referencing; `CANONICAL_BANDS` |
|
|
193
|
+
| `connectivity.py` | Mutual information, Granger causality, phase slope index, transfer entropy; uniform `DirectedResult` |
|
|
194
|
+
| `decoding.py` | Nested cross-validated linear-SVM population decoding (accuracy/F1/AUC/majority-baseline) |
|
|
195
|
+
| `spiking.py` | Spike-response firing rate/latency/z-score, significance classification, spike-LFP phase locking |
|
|
196
|
+
| `viz.py` | Vector-graphics setup, tight auto-scaled axes, multi-page/format figure export, onset resampling, array-in PSTH |
|
|
197
|
+
| `mcp_server/` | stdio MCP server: `inspect_nwb`, `get_event_codes_and_timings`, `prepare_signal_reference`, `add_tool` |
|
|
198
|
+
|
|
199
|
+
Task-specific functionality (condition codes, unit classification, decoding, connectivity,
|
|
200
|
+
figure suites) lives in [`omission/jnwb_ext/`](omission/README.md), not here.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## MCP server
|
|
205
|
+
|
|
206
|
+
`jnwb` includes a stdio Model Context Protocol server for NWB inspection from Claude and other
|
|
207
|
+
MCP-compatible clients: `inspect_nwb`, `get_event_codes_and_timings`, `prepare_signal_reference`,
|
|
208
|
+
`add_tool`. Depends on `mcp`, `h5py`, `pynwb`, `pandas`, `numpy` (installed via `pip install -e .`).
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
python -m jnwb.mcp_server
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"mcpServers": {
|
|
217
|
+
"jnwb-mcp-server": {
|
|
218
|
+
"command": "python",
|
|
219
|
+
"args": ["-m", "jnwb.mcp_server"]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Repository layout
|
|
228
|
+
|
|
229
|
+
| Path | Contents |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `jnwb/` | The library (above) |
|
|
232
|
+
| `tests/` | Pytest suite for the generic library — run it, don't trust pass counts in docs |
|
|
233
|
+
| `omission/` | The example project built on `jnwb` — see [`omission/README.md`](omission/README.md) |
|
|
234
|
+
| `.claude/skills/` | Task-scoped API guides |
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Before you change anything
|
|
239
|
+
|
|
240
|
+
- **`CLAUDE.md`** — repo doctrine: library invariants, footguns, verification checks that caught
|
|
241
|
+
real errors.
|
|
242
|
+
- **`omission/.claude/skills/`** — task-scoped API guides (`omission-data`, `omission-signal`,
|
|
243
|
+
`omission-spiking`, `omission-statistics`, `omission-figures`, `manuscript`, `labyrinth`). There
|
|
244
|
+
is no repo-root `.claude/skills/` — `jnwb/` itself has no dedicated skill yet (see
|
|
245
|
+
`numerical-computing` / `biophysical-modeling`, which are general-purpose, not jnwb-specific).
|