Skip to main content

jnwb

Dataset-agnostic Python library for NWB (Neurodata Without Borders) electrophysiology analysis: session I/O, addressing (channel→area, depth→layer), representational similarity analysis (JRSA), TFR accumulation/compression, generic statistics, and visual QC.

jnwb makes no assumptions about task structure, condition codes, or experiment design. For a worked example of building a full project on top of it — including a task-specific extension package, scripts, notebooks, and a manuscript pipeline — see omission/, this repo's native large-dataset example project.


Install

pip install -e ".[test]"

Paths — do this first after any drive remap

Repo-internal paths (REPO_ROOT, outputs_dir(), artifacts_dir()) resolve from the package's own location and always work. External data roots live on a separate volume and are set by environment variable — see jnwb/paths.py for the full list and defaults.

import jnwb
jnwb.paths.describe()   # every root + whether it currently resolves

If a root shows exists: false, set its env var — do not edit source, and do not write a new absolute literal into a script.


Quick start

import jnwb

result = jnwb.jrsa(x1, x2, metric='rsa', stats=True)
result.summary()
result.plot()

Null construction under an explicit exchangeability scheme — every call must name scheme ("within_group" or "global"), there is no default, since a bare rng.permutation(y) inside grouped/session-structured decoding is a documented past bug (see jnwb/permutation.py):

import numpy as np
rng = np.random.default_rng(0)
null_labels = jnwb.permute_labels(labels, groups=cycle_id, scheme="within_group", rng=rng)

Trial-segmented LFP/TFR artifact detection-and-substitution (cross-channel-synchrony detection, cross-trial-median repair; see jnwb/artifact_repair.py):

repaired, frac_flagged, diagnostics = jnwb.repair_lfp_trials(
    segments, times_ms=times_ms, z_thresh=6.0)          # segments: (n_trials, n_channels, n_times)
repaired_power, frac_flagged_by_band = jnwb.repair_band_artifacts(power, freqs)  # per-band TFR

Causal PSTH smoothing and causality-bounded exponential onset-latency fit (a forward-only kernel by design — an acausal/centered smoother would let post-onset activity bias the fitted onset earlier than the true rise; see jnwb/onset_fitting.py):

smoothed = jnwb.causal_exp_smooth(rate, bin_ms=5.0, tau_ms=30.0)
fit = jnwb.fit_exponential_onset(t_ms, smoothed, t0_bounds=(0.0, None))  # fit["t0"], ["tau"], ["r2"]

Unit/electrode metadata extraction, QC classification, and census reporting, from a plain list of NWB paths:

units = jnwb.get_all_units_metadata(nwb_paths)          # -> DataFrame, one row per unit
units = jnwb.classify_unit_quality(units)                # + quality_class, is_valid, issue_flags
census = jnwb.unit_census_report(units, group_by=["area"])
snr_stats = jnwb.get_snr_analysis(units)                 # -> {'pass_rate': ..., 'snr_mean': ...}
good_v1 = jnwb.filter_by_criteria(units, {"area": "V1", "firing_rate": (1.0, 50.0)})
unit_audit = jnwb.audit_units(units)             # spike-time coverage, quality/SNR/rate stats
elec_audit = jnwb.audit_electrodes(electrodes, units)  # area counts, unit-assignment rate
tier = jnwb.assign_quality_tier(units["quality"], units["trial_presence_fraction"], units["snr"])
diff = jnwb.compare_old_new_criteria(new_units, old_units)  # gained/lost/unchanged transitions

Generic paired fire-probability testing — plain spike-time/onset arrays and boolean pairs in, a shuffle-null p-value + bootstrap CI + odds ratio out (jnwb/statistics.py):

fired = jnwb.fire_indicator(spike_times, onsets, window_ms=(0.0, 150.0))
result = jnwb.paired_fire_prob_test(fired_target, fired_baseline, n_shuffles=2000, n_bootstrap=2000, rng=rng)

Generic spike-rate windowing, shuffle-controlled paired/unpaired p-values, temporal cycle/quantile detection on a trial table, and a shuffle-null R² CI (jnwb/statistics.py):

rate_hz = jnwb.rate_in_window(spike_times, onset_s, window_ms=(0.0, 200.0))
obs, p = jnwb.shuffle_pvalue_paired(a, b, n_shuffles=2000, rng=rng, alternative="greater")
cycle_id = jnwb.detect_trial_cycles(trials, gap_factor=10.0)     # trials: DataFrame with start_time
r2 = jnwb.shuffle_r2_ci(y_true, y_score, groups=cycle_id, n_shuffle=500)
result = jnwb.cross_modal_comparison(tfr_data, spike_data)       # trial-averaged zero-lag correlation
result = jnwb.cross_modal_comparison(tfr_data, spike_data, lag_range_ms=(-200, 200), bin_ms=10.0)
# ^ with bin_ms given, searches lag_range_ms for the best-correlating shift instead of zero-lag only

Generic spectral analysis — band-limited power, cross-area coherence, 1/f tilt, imaginary coherency (immune to zero-lag volume-conduction mixing by construction), and bipolar/Laplacian re-referencing (jnwb/spectral.py; CANONICAL_BANDS is the single-source-of-truth band-edge default, theta/alpha/beta/low_gamma/high_gamma):

coh = jnwb.cross_area_coherence(v1_lfp, pfc_lfp, sampling_rate=1000.0)
theta_power_db = jnwb.band_power(lfp, 1000.0, jnwb.CANONICAL_BANDS["theta"], baseline=baseline_lfp)
icoh = jnwb.imaginary_coherency(x, y, sampling_rate=1000.0, freq_range=(1, 100))  # icoh_mean, coh_mag_mean
laplacian = jnwb.laplacian_reference(channel_data, channel_order=depth_order)
freqs, psd = jnwb.compute_psd(lfp_data, fs=1000.0)   # plain Welch PSD wrapper

Modality-agnostic directed functional connectivity — Granger causality, spectral (Geweke) Granger, phase slope index, and transfer entropy all share one (X, Y, ...) contract and return the same DirectedResult shape, so LFP traces, binned spike counts, MUAe envelopes, and band-power time courses go through identical code (jnwb/connectivity.py):

rate_a = jnwb.bin_spikes(spike_times_a, window=(-0.5, 1.0), bin_size_ms=10.0, output="rate")
result = jnwb.granger(v1_lfp, pfc_lfp, order="auto")        # or jnwb.phase_slope_index(..., fs=1000.0)
print(result.x_to_y, result.y_to_x, result.net)              # DirectedResult: uniform across estimators
network = jnwb.directed_network({"V1": v1_lfp, "PFC": pfc_lfp}, method="granger")

Generic nested cross-validated linear-SVM population decoding — plain (n_trials, n_features) matrix and integer labels in, accuracy/F1/AUC/majority-baseline out, NaN (never fabricated) under degenerate class counts (jnwb/decoding.py):

result = jnwb.nested_cv_linear_svm(X, labels, n_splits=5)
print(result["accuracy"], result["f1"], result["auc"], result["majority_baseline_accuracy"])

Generic grouped leave-one-group-out CV geometry, representation contracts (R0/R1/R2), and a reproducible within-group null-permutation plan — plain trial DataFrame/array in, no session or condition semantics (jnwb/decoding.py, jnwb/permutation.py):

outer = jnwb.assign_outer_folds(trials, analysis_cols=("session", "analysis", "slot_key"), group_col="cycle")
inner = jnwb.build_inner_validation_partitions(outer)
ladder = jnwb.build_representation_ladder(raster, modality="SPK")   # X_rate, X_vec, X_structured
plan = jnwb.build_permutation_plan(labels, groups, n_permutations=1000, seed=0)  # draw_manifest

Generic spike-response metrics — firing rate/latency/z-score relative to any behavioral epoch, significance classification, and spike-LFP phase locking (jnwb/spiking.py):

metrics = jnwb.compute_response_metrics(spike_times, epoch_onsets, response_window=(0.0, 0.15))
sig = jnwb.classify_response_significance(metrics)             # is_significant, pvalue, confidence
pli = jnwb.phase_locking_index(spike_times, lfp_phase, lfp_timestamps)  # pli, rayleigh_pvalue

Generic plotting utilities — vector-graphics setup, tight auto-scaled axes, multi-page/format figure export, trial-onset resampling, and array-in PSTH (jnwb/viz.py):

jnwb.setup_vector_graphics()                                    # editable SVG fonts
centers, mean_hz, sem_hz = jnwb.raster_psth(spike_times, onsets, win_ms=(-500, 1000))
jnwb.save_figure_suite(figures, "outputs/figures", basename="raster", formats=["png", "pdf"])

Module map

The public surface is jnwb/__init__.py (__all__).

Module Role
paths.py Repo and data root resolution; the only place absolute paths live
addressing.py Peak-channel → area mapping, depth → layer classification
ontology.py, jrsa.py Dataset/AlignedDataset/Question/Result objects; unified RSA engine
statistics.py, analyzers.py StatisticalAnalysis, TFRAnalyzer, UnitAnalyzer, PopulationAnalyzer
tfr_accumulator.py, compression.py Poolable TFR summary statistics; NWB fp32 compression
trajectory.py, gpu_pca.py Population trajectories via GPU SVD
visual_qc.py Generic visual QC plotting
bilinear.py, nam.py, permutation.py Generic modeling/statistical primitives
artifact_repair.py, artifact_detection.py Trial-segmented artifact repair (substitution) / detection (exclusion)
onset_fitting.py Causal PSTH smoothing; causality-bounded exponential onset-latency fit
metadata.py Unit/electrode metadata extraction, QC classification, census reporting
spectral.py Band power, cross-area coherence, 1/f tilt, imaginary coherency, bipolar/Laplacian re-referencing; CANONICAL_BANDS
connectivity.py Mutual information, Granger causality, phase slope index, transfer entropy; uniform DirectedResult
decoding.py Nested cross-validated linear-SVM population decoding (accuracy/F1/AUC/majority-baseline)
spiking.py Spike-response firing rate/latency/z-score, significance classification, spike-LFP phase locking
viz.py Vector-graphics setup, tight auto-scaled axes, multi-page/format figure export, onset resampling, array-in PSTH
mcp_server/ stdio MCP server: inspect_nwb, get_event_codes_and_timings, prepare_signal_reference, add_tool

Task-specific functionality (condition codes, unit classification, decoding, connectivity, figure suites) lives in omission/jnwb_ext/, not here.


MCP server

jnwb includes a stdio Model Context Protocol server for NWB inspection from Claude and other MCP-compatible clients: inspect_nwb, get_event_codes_and_timings, prepare_signal_reference, add_tool. Depends on mcp, h5py, pynwb, pandas, numpy (installed via pip install -e .).

python -m jnwb.mcp_server
{
  "mcpServers": {
    "jnwb-mcp-server": {
      "command": "python",
      "args": ["-m", "jnwb.mcp_server"]
    }
  }
}

Repository layout

Path Contents
jnwb/ The library (above)
tests/ Pytest suite for the generic library — run it, don't trust pass counts in docs
omission/ The example project built on jnwb — see omission/README.md
.claude/skills/ Task-scoped API guides

Before you change anything

  • CLAUDE.md — repo doctrine: library invariants, footguns, verification checks that caught real errors.
  • omission/.claude/skills/ — task-scoped API guides (omission-data, omission-signal, omission-spiking, omission-statistics, omission-figures, manuscript, labyrinth). There is no repo-root .claude/skills/ — jnwb/ itself has no dedicated skill yet (see numerical-computing / biophysical-modeling, which are general-purpose, not jnwb-specific).

Release files for jnwb 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jnwb 0.1.0
File Size Uploaded
jnwb-0.1.0.tar.gz 205.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for jnwb 0.1.0
File Interpreter ABI Platform
jnwb-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 361.4 kB

Release files / jnwb-0.1.0.tar.gz

Download URL jnwb-0.1.0.tar.gz
Size 205.0 kB
Tags Source
SHA-256 checksum
How to use checksums
0b2793779c1d7150af78a8375d83f9a208998f6b5d4680db8b719f1509462587
BLAKE2b-256 checksum
How to use checksums
b0bee4faeeae9122ad66300573b06a0eb107ddd7bb95e66d917b4e3871ef87fa
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 Sep 2, 2026.

Transparency log

Release files / jnwb-0.1.0-py3-none-any.whl

Download URL jnwb-0.1.0-py3-none-any.whl
Size 156.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0297a13d40b82e4769109faaca76aa9b2b10baec8d3a69ccb73f45feae592b07
BLAKE2b-256 checksum
How to use checksums
1b4c3490b42cad9897b796d5428bd4fb3cbcc043f038bc8cf9bf7c84c3b20873
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 Sep 2, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page