jnwb
Dataset-agnostic Python library for Neurodata Without Borders (NWB 2.0+) electrophysiology: addressing, spikes, LFP, spectral analysis, statistics, population methods, decoding, connectivity, laminar depth (CSD, the published vFLIP2), filtering, QC, and visualization.
Documentation: https://jnwb.readthedocs.io/ | Source: https://github.com/HNXJ/jnwb
jnwb is a toolbox, not a pipeline. It supplies small operations over NWB files, arrays, and metadata tables. Task structure, condition codes, and experimental hypotheses stay in project code.
- Dataset-agnostic. No experiment condition names or manuscript results live in the library.
- Explicit nulls. Label permutation requires an exchangeability scheme (
globalorwithin_group). - Preserved signal semantics. Units, sampling rates, coordinate frames, and 0- vs 1-indexing do not change across a function boundary.
Capabilities
| Area | Representative API |
|---|---|
| NWB discovery & events | inspect, events, event_onsets, unit_spike_times, acquisition_channel |
| NWB metadata & addressing | get_all_units_metadata, electrode_inventory, map_peak_channel_to_area, classify_layer_from_depth |
| Spiking | raster_psth, compute_response_metrics, causal_exp_smooth, fit_exponential_onset, pairwise_phase_consistency |
| LFP & spectral | compute_psd, compute_multitaper_psd, band_power, complex_tfr, aggregate_to_db, current_source_density_1d |
| Filtering | bandpass_filter, notch_filter |
| Statistics | permute_labels, cluster_permutation_test, shuffle_pvalue_paired, paired_fire_prob_test |
| Population & decoding | jrsa, nested_cv_linear_svm, compute_population_trajectory |
| Connectivity | granger, phase_slope_index, transfer_entropy, directed_network |
| Quality control | channel_correlation_matrix, repair_lfp_trials, audit_units, audit_electrodes |
| Waveform classes | waveform_features, normalize_waveforms, wavemap, wavemap_resolution_sweep |
| Visualization | setup_vector_graphics, save_figure_suite, plotting in jnwb.vis |
Installation
Requires Python 3.12 or newer. Tested in CI on 3.12, 3.13, 3.14 and 3.15 on Linux, and on Windows for all but the last.
pip install jnwb # latest published release
pip install "jnwb[torch,gpu]" # optional backends
This checkout is 0.2.12. To install it from a clone instead of from PyPI:
pip install . # or: pip install -e ".[test,docs,vis]" for development
The vis extra exports SVG and PNG through kaleido, which needs a Chrome or Chromium: see Install.
Core dependencies: numpy, scipy, pandas, h5py, pynwb, hdmf, matplotlib, scikit-learn, statsmodels, joblib.
NWB workflow
jnwb.inspect lists acquisitions, electrodes, units, and every interval table with column samples. It does not pick a default event table. Event codes are opaque labels in a named column (default codes). Onsets from jnwb.event_onsets are in seconds. When several interval tables exist, pass table= explicitly; jnwb raises rather than guessing.
import jnwb
info = jnwb.inspect("session.nwb")
# Read the layout off the inspection rather than assuming it.
for table in info["interval_tables"]:
print(table["name"], [column["name"] for column in table["columns"]])
# trials ['id', 'start_time', 'stimulus', 'stop_time']
table = jnwb.events("session.nwb", table="trials", code_column="stimulus")
onsets = jnwb.event_onsets(
"session.nwb", table="trials", code_column="stimulus", codes=["grating"],
)
The codes default, and the errors for a missing column, are on the Errors page. On a table with no codes column, events returns the onsets and warns; event_onsets without codes= returns them silently.
Executable walkthroughs: Read the Docs tutorials or examples/tutorials/.
Quickstart (arrays)
import numpy as np
import jnwb
rng = np.random.default_rng(42)
events = np.arange(1.0, 21.0, 0.5) # 40 trials, 0.5 s apart
lags = 0.060 + rng.uniform(0.0, 0.34, (events.size, 25)) # each responds from t0 = 60 ms
spikes = np.sort(np.concatenate([rng.uniform(0.0, 21.5, 110), # homogeneous background
(events[:, None] + lags).ravel()]))
time_bins, rate_hz, _ = jnwb.raster_psth(spikes, events, win_ms=(-100.0, 400.0), bin_ms=10.0)
smooth_hz = jnwb.causal_exp_smooth(rate_hz, bin_ms=10.0, tau_ms=25.0)
fit = jnwb.fit_exponential_onset(time_bins, smooth_hz, t0_bounds_ms=(0.0, 200.0))
print(f"Onset t0: {fit['t0']:.1f} ms of a true 60.0 "
f"(R2={fit['r2']:.2f}, {fit['bound_status'] or 'interior'})")
fs = 1000.0
lfp = rng.normal(size=1000)
tfr = jnwb.complex_tfr(lfp, fs=fs, freqs=np.linspace(10.0, 60.0, 10))
beta = jnwb.band_power(lfp, fs=fs, freq_range=jnwb.CANONICAL_BANDS["beta"], normalize=False)
print(f"TFR shape: {tfr.shape}, beta power: {beta:.4f}")
Read spikes and LFP for alignment after you have onsets. Onsets are session time; sample 0
of the LFP is at its starting_time, or at its first timestamp when the series stores
timestamps (starting_time is None and acquisition_channel raises, so the samples are
read from the file):
import numpy as np
from pynwb import NWBHDF5IO
spikes = jnwb.unit_spike_times("session.nwb", unit_index=0)
entry = next(a for a in info["acquisitions"] if a["name"] == "probe_0_lfp")
if entry["starting_time"] is not None: # stored with starting_time and rate
lfp, fs_hz = jnwb.acquisition_channel("session.nwb", name="probe_0_lfp", channel=0)
start_s = entry["starting_time"]
else: # stored with timestamps, sampled regularly
with NWBHDF5IO("session.nwb", "r") as io:
series = io.read().acquisition["probe_0_lfp"]
t, lfp = series.timestamps[:], series.data[:, 0] # entry["layout"], raw samples
fs_hz, start_s = 1.0 / np.median(np.diff(t)), float(t[0])
epochs, t_axis_s = jnwb.epoch_continuous(lfp, onsets - start_s, win_s=(-0.1, 0.4), fs=fs_hz)
Unit and electrode census:
units = jnwb.get_all_units_metadata("session.nwb")
electrodes = jnwb.electrode_inventory("session.nwb")
More examples
Each block plants a known effect in synthetic data and prints what jnwb recovers.
Direction between two signals (x leads y by 3 samples):
import numpy as np
import jnwb
rng = np.random.default_rng(0)
x = rng.normal(size=5000)
y = 0.6 * np.roll(x, 3) + rng.normal(size=5000)
gc = jnwb.granger(x, y, order=5)
print(f"x->y {gc.x_to_y:.3f} (p={gc.p_x_to_y:.3f}), y->x {gc.y_to_x:.3f} (p={gc.p_y_to_x:.3f})")
# x->y 0.311 (p=0.000), y->x 0.001 (p=0.452)
A condition difference in time, tested with a cluster permutation (Maris & Oostenveld, 2007):
import numpy as np
import jnwb
rng = np.random.default_rng(1)
a = rng.normal(size=(20, 100)) # trials x time points
b = rng.normal(size=(20, 100))
b[:, 40:60] += 1.5 # effect at points 40-59
res = jnwb.cluster_permutation_test(a, b, n_permutations=500, rng=1)
best = min(res["clusters"], key=lambda c: c["p_value"])
span = np.flatnonzero(best["mask"])
print(f"cluster {span.min()}-{span.max()}, p = {best['p_value']:.3f}")
# cluster 39-59, p = 0.002
Decoding against a label-permutation null with a named scheme:
import numpy as np
import jnwb
rng = np.random.default_rng(2)
labels = np.repeat([0, 1], 40)
X = rng.normal(size=(80, 30)) # trials x units
X[labels == 1, :5] += 1.2 # 5 informative units
dec = jnwb.nested_cv_linear_svm(X, labels, n_splits=5, rng=2)
shuffled = jnwb.permute_labels(labels, scheme="global", rng=3)
null = jnwb.nested_cv_linear_svm(X, shuffled, n_splits=5, rng=2)
print(f"accuracy {dec['accuracy']:.2f}, shuffled {null['accuracy']:.2f}, "
f"majority {dec['majority_baseline_accuracy']:.2f}")
# accuracy 0.88, shuffled 0.42, majority 0.50
Documentation
Guides, the public API (every symbol in jnwb.__all__), and common mistakes are on Read the Docs.
Contributing
Setup, the checks to run, the branch model and the release procedure are in
CONTRIBUTING.md. Work lands on dev; main holds releases.
For AI agents: see artifacts/agents.md.
License
MIT. See LICENSE.
Metadata
Release files for jnwb 0.2.12
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jnwb-0.2.12.tar.gz | 539.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jnwb-0.2.12-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.1 MB
Release files / jnwb-0.2.12.tar.gz
| Download URL | jnwb-0.2.12.tar.gz |
|---|---|
| Size | 539.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c9c17ac4b82821a572133396dfa682fe82f525b2e68d0d5fc104260140f887ca
|
|
BLAKE2b-256 checksum How to use checksums |
7874c97b617aca868fc6be05839734b7a23260f5ae8a38909b3a00745b14ba7e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency logRelease files / jnwb-0.2.12-py3-none-any.whl
| Download URL | jnwb-0.2.12-py3-none-any.whl |
|---|---|
| Size | 542.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
904d6247706ec06c76c7e0c7a2c4a7a0dbd4d700242de19b282f0f5f134eb08f
|
|
BLAKE2b-256 checksum How to use checksums |
c8bec970bc4970e4078f3d097465c9416c36966e897d58331cf32a7522efb449
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency log