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.
Files changed (75) hide show
  1. jnwb-0.1.0/LICENSE +21 -0
  2. jnwb-0.1.0/PKG-INFO +303 -0
  3. jnwb-0.1.0/README.md +245 -0
  4. jnwb-0.1.0/jnwb/__init__.py +433 -0
  5. jnwb-0.1.0/jnwb/addressing.py +193 -0
  6. jnwb-0.1.0/jnwb/analyzers.py +750 -0
  7. jnwb-0.1.0/jnwb/artifact_detection.py +103 -0
  8. jnwb-0.1.0/jnwb/artifact_repair.py +350 -0
  9. jnwb-0.1.0/jnwb/bilinear.py +155 -0
  10. jnwb-0.1.0/jnwb/compression.py +525 -0
  11. jnwb-0.1.0/jnwb/connectivity.py +2025 -0
  12. jnwb-0.1.0/jnwb/decoding.py +344 -0
  13. jnwb-0.1.0/jnwb/gpu_pca.py +95 -0
  14. jnwb-0.1.0/jnwb/jrsa.py +1552 -0
  15. jnwb-0.1.0/jnwb/mcp_server/__init__.py +15 -0
  16. jnwb-0.1.0/jnwb/mcp_server/custom_tools.py +4 -0
  17. jnwb-0.1.0/jnwb/mcp_server/event_tools.py +148 -0
  18. jnwb-0.1.0/jnwb/mcp_server/meta_tools.py +76 -0
  19. jnwb-0.1.0/jnwb/mcp_server/nwb_tools.py +138 -0
  20. jnwb-0.1.0/jnwb/mcp_server/server.py +3 -0
  21. jnwb-0.1.0/jnwb/metadata.py +575 -0
  22. jnwb-0.1.0/jnwb/nam.py +118 -0
  23. jnwb-0.1.0/jnwb/onset_fitting.py +277 -0
  24. jnwb-0.1.0/jnwb/ontology.py +394 -0
  25. jnwb-0.1.0/jnwb/paths.py +239 -0
  26. jnwb-0.1.0/jnwb/permutation.py +135 -0
  27. jnwb-0.1.0/jnwb/py.typed +0 -0
  28. jnwb-0.1.0/jnwb/spectral.py +685 -0
  29. jnwb-0.1.0/jnwb/spiking.py +258 -0
  30. jnwb-0.1.0/jnwb/statistics.py +1036 -0
  31. jnwb-0.1.0/jnwb/tfr.py +224 -0
  32. jnwb-0.1.0/jnwb/tfr_accumulator.py +122 -0
  33. jnwb-0.1.0/jnwb/trajectory.py +191 -0
  34. jnwb-0.1.0/jnwb/visual_qc.py +350 -0
  35. jnwb-0.1.0/jnwb/viz.py +136 -0
  36. jnwb-0.1.0/jnwb.egg-info/PKG-INFO +303 -0
  37. jnwb-0.1.0/jnwb.egg-info/SOURCES.txt +105 -0
  38. jnwb-0.1.0/jnwb.egg-info/dependency_links.txt +1 -0
  39. jnwb-0.1.0/jnwb.egg-info/requires.txt +36 -0
  40. jnwb-0.1.0/jnwb.egg-info/top_level.txt +1 -0
  41. jnwb-0.1.0/pyproject.toml +106 -0
  42. jnwb-0.1.0/setup.cfg +4 -0
  43. jnwb-0.1.0/tests/test_addressing.py +138 -0
  44. jnwb-0.1.0/tests/test_analyzers_coverage.py +350 -0
  45. jnwb-0.1.0/tests/test_artifact_detection.py +112 -0
  46. jnwb-0.1.0/tests/test_artifact_repair.py +131 -0
  47. jnwb-0.1.0/tests/test_audit_reproducers.py +53 -0
  48. jnwb-0.1.0/tests/test_batch_a_regressions.py +289 -0
  49. jnwb-0.1.0/tests/test_complex_tfr.py +301 -0
  50. jnwb-0.1.0/tests/test_compression.py +83 -0
  51. jnwb-0.1.0/tests/test_connectivity.py +174 -0
  52. jnwb-0.1.0/tests/test_decoding.py +193 -0
  53. jnwb-0.1.0/tests/test_docs_smoke.py +157 -0
  54. jnwb-0.1.0/tests/test_gpu_pca.py +49 -0
  55. jnwb-0.1.0/tests/test_harness_adversarial_gates.py +93 -0
  56. jnwb-0.1.0/tests/test_jnwb_core.py +295 -0
  57. jnwb-0.1.0/tests/test_jnwb_frozen_boundary.py +116 -0
  58. jnwb-0.1.0/tests/test_jrsa_correctness.py +47 -0
  59. jnwb-0.1.0/tests/test_jrsa_gpu.py +60 -0
  60. jnwb-0.1.0/tests/test_jrsa_no_fabricated_failures.py +68 -0
  61. jnwb-0.1.0/tests/test_mcp_server.py +161 -0
  62. jnwb-0.1.0/tests/test_metadata.py +280 -0
  63. jnwb-0.1.0/tests/test_onset_fitting.py +105 -0
  64. jnwb-0.1.0/tests/test_paths.py +83 -0
  65. jnwb-0.1.0/tests/test_permutation.py +156 -0
  66. jnwb-0.1.0/tests/test_representative_workflow.py +213 -0
  67. jnwb-0.1.0/tests/test_spectral.py +194 -0
  68. jnwb-0.1.0/tests/test_spiking.py +87 -0
  69. jnwb-0.1.0/tests/test_statistics.py +296 -0
  70. jnwb-0.1.0/tests/test_statistics_api_split.py +176 -0
  71. jnwb-0.1.0/tests/test_tfr_accumulator.py +244 -0
  72. jnwb-0.1.0/tests/test_tfr_extract_band_regressions.py +170 -0
  73. jnwb-0.1.0/tests/test_trajectory.py +119 -0
  74. jnwb-0.1.0/tests/test_visual_qc.py +97 -0
  75. 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).