Dataset-agnostic Python library for Neurodata Without Borders (NWB 2.0+) electrophysiology: addressing, spikes, LFP, spectral analysis, statistics, population methods, decoding, connectivity, laminar CSD, 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.
| 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 |
| Visualization | raster_psth, setup_vector_graphics, save_figure_suite |
Requires Python 3.12 or newer. Tested in CI on 3.12, 3.13 and 3.14.
pip install jnwb # latest published release
pip install "jnwb[torch,gpu]" # optional backendsThis checkout is 0.2.8. To install it from a clone instead of from PyPI:
pip install . # or: pip install -e ".[test,docs]" for developmentCore dependencies: numpy, scipy, pandas, h5py, pynwb, hdmf, matplotlib, scikit-learn, statsmodels, joblib.
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"],
)codes is jnwb's default column name, not an NWB requirement — a file from another lab
usually names it something else, which is why the column comes from inspect rather than
from habit. Naming a column that does not exist raises ColumnNotFoundError listing the
columns that do. 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/.
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:
spikes = jnwb.unit_spike_times("session.nwb", unit_index=0)
lfp, fs_hz = jnwb.acquisition_channel("session.nwb", name="probe_0_lfp", channel=0)
start_s = next(a["starting_time"] for a in info["acquisitions"] if a["name"] == "probe_0_lfp")
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")Guides, the public API (every symbol in jnwb.__all__), and common mistakes are on Read the Docs.
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.
MIT. See LICENSE.
