Back to the 5-minute intro

nstat.extras

Opt-in Python bridges between the MATLAB-faithful nSTAT core and the modern Python systems-neuroscience ecosystem — without coupling the core parity contract.

Part of nSTAT-python. See the 5-minute intro for a guided tour, the Sphinx Extras index for per-bridge usage guides, and the core API reference.

What nstat.extras is

The core nstat.* namespace mirrors the MATLAB nSTAT toolbox method-for-method and is under a strict parity contract — class names, method names, and numerical conventions match upstream MATLAB exactly so users can port code without surprises. That contract is the reason core nstat has zero dependencies outside NumPy / SciPy / SymPy / matplotlib.

nstat.extras.* is the home for everything that doesn't fit the parity contract: bridges to the wider Python neural-data stack, modern decoders, validation oracles, and metrics with no MATLAB counterpart. Every extras module is opt-in: it requires an explicit pip install nstat-toolbox[…] and follows a different stability tier (minor-version evolution, not major-version).

Pattern modeled on scikit-learn-contrib, but in-tree rather than as a separate package — preserves discoverability and lets the test suite enforce the install-hint contract.

Three tiers

A Validation

nstat.extras.validation.* — Python-side cross-validation oracles. Fits the same statistical model in nstat and an independent reference library, then returns a comparison object with assert_agree(atol, rtol) hooks for parity tests.

Strongest available cross-check on nstat's GLM & Kalman paths short of running MATLAB-engine.

B Interop

nstat.extras.interop.*nspikeTrain / SpikeTrainCollection / Trial converters for Neo, pynapple, and NWB. Unlocks Spike2 / NEX / Blackrock / Plexon / TDT readers via Neo; epoch-math via pynapple; BRAIN-Initiative NWB:N format.

Bridges the file-format gap between MATLAB workflows and modern Python pipelines.

C Metrics

nstat.extras.metrics.* — modern spike-train distance metrics with no MATLAB counterpart. Thin wrappers around PySpike for ISI / SPIKE / SPIKE-synchronization (Kreuz family, parameter-free).

C/Cython-accelerated; suitable for population-level distance matrices.

Install matrix

GroupPullsBacks
[neo]neo, quantitiesextras.interop.neo
[pynapple]pynappleextras.interop.pynapple
[nwb]pynwb (and hdmf)extras.interop.nwb
[metrics]pyspikeextras.metrics.spike_distances
[nemos]nemos (incl. JAX, ~200 MB)extras.validation.nemos_bridge
[test-parity]nemos + pykalman + statsmodels + nitimeAll cross-validation bridges
[dynamax]dynamax (incl. JAX, ~200 MB)extras.em.dynamax_bridge
[all-extras]Every functional group above except [dynamax]One-shot install (heavy [dynamax] opt-out documented in tests/test_pyproject_consistency.py::HEAVY_OPT_OUT_OF_ALL_EXTRAS)

The [all-extras] union is CI-enforced — adding a new group without updating [all-extras] fails the test suite.

Per-bridge cards

B nstat.extras.interop.neo

pip install nstat-toolbox[neo]

Round-trip between nspikeTrain and neo.SpikeTrain; build neo.Segment from a collection. Unlocks Spike2 / NEX / Blackrock / Plexon / TDT via Neo readers.

from nstat.extras.interop.neo import to_neo_spiketrain, from_neo_spiketrain

neo_st = to_neo_spiketrain(nst)          # nstat → Neo
nst_back = from_neo_spiketrain(neo_st)   # Neo → nstat

Details: interop_neo.html · Demo: interop_neo_demo.py

B nstat.extras.interop.pynapple

pip install nstat-toolbox[pynapple]

Round-trip between nspikeTrain and pynapple.Ts; preserves recording window via IntervalSet; supports epoch math like ts.restrict(IntervalSet).

import pynapple as nap
from nstat.extras.interop.pynapple import to_pynapple_with_support, from_pynapple_ts

ts, support = to_pynapple_with_support(nst)
ts_sub = ts.restrict(nap.IntervalSet(start=2.0, end=5.0))
nst_sub = from_pynapple_ts(ts_sub, name="sub", sample_rate=30_000, support=...)

Details: interop_pynapple.html · Demo: interop_pynapple_demo.py

B nstat.extras.interop.nwb

pip install nstat-toolbox[nwb]

Read NWB:N files into SpikeTrainCollection. Resolves per-unit observation windows via obs_intervals (NWB standard) or an explicit time_window= override; emits UserWarning when neither is available.

from nstat.extras.interop.nwb import read_nwb_path

coll = read_nwb_path("/path/to/session.nwb", sample_rate=30_000.0)
# Or: nwb_units_to_collection(nwbfile, time_window=(0.0, 600.0))

Details: interop_nwb.html · Demo: interop_nwb_demo.py

A nstat.extras.validation.nemos_bridge

pip install nstat-toolbox[nemos] (or [test-parity])

Cross-validate nstat.fit_poisson_glm against Flatiron's NeMoS GLM. Returns a GLMComparison with assert_agree hook for parity tests.

from nstat.extras.validation.nemos_bridge import cross_validate_poisson_glm

cmp = cross_validate_poisson_glm(X, y)
cmp.assert_agree(atol=5e-2, rtol=5e-2)

Details: validation_nemos.html · Demo: validation_nemos_demo.py

A nstat.extras.validation.statsmodels_bridge

pip install nstat-toolbox[test-parity]

Cross-validate nstat.fit_poisson_glm against statsmodels' IRLS. Because both use IRLS, agreement is typically ~1e-9 (machine precision) — the tightest oracle in nstat.extras.validation.

from nstat.extras.validation.statsmodels_bridge import cross_validate_poisson_glm

cmp = cross_validate_poisson_glm(X, y)
cmp.assert_agree(atol=1e-6, rtol=1e-6)   # tight by design

Details: validation_statsmodels.html · Demo: validation_statsmodels_demo.py

A nstat.extras.validation.pykalman_bridge

pip install nstat-toolbox[test-parity]

Cross-validate DecodingAlgorithms.kalman_filter + kalman_fixedIntervalSmoother against pykalman. Documents the AUDIT D3 smoother-gap empirically (~0.4 unit baseline on a 100×2 LG fixture).

from nstat.extras.validation.pykalman_bridge import cross_validate_kalman

cmp = cross_validate_kalman(y, A, C, Q, R, x0, P0)
cmp.assert_filtered_agree(atol=1e-2)  # filter agrees
# Smoother gap is documented & known — see AUDIT D3.

Details: validation_pykalman.html · Demo: validation_pykalman_demo.py

C nstat.extras.metrics.spike_distances

pip install nstat-toolbox[metrics]

ISI-distance (Kreuz 2007), SPIKE-distance (Kreuz 2013), SPIKE-synchronization (Kreuz 2015) and pairwise N×N distance matrix. Parameter-free — no kernel bandwidth, no binning.

from nstat.extras.metrics.spike_distances import (
    spike_distance, pairwise_spike_distance_matrix,
)

d = spike_distance(a, b)                   # scalar in [0, 1]
D = pairwise_spike_distance_matrix(trains)  # (N, N)

Details: metrics_spike_distances.html · Demo: metrics_spike_distances_demo.py

A nstat.extras.em.dynamax_bridge

pip install nstat-toolbox[dynamax] (pulls JAX, ~200 MB)

State-space estimation closing the unported MATLAB KF_EM / PP_EM / mPPCO_EM families (AUDIT_REPORT.md §3.2 — 19 methods / ~7,500 LOC of MATLAB). Three EM trainers + two point-process inference routines:

  • fit_linear_gaussian_em — KF_EM equivalent (thin Dynamax LinearGaussianSSM.fit_em wrapper).
  • cmgf_poisson_filter / cmgf_poisson_smoother — point-process Kalman filter/smoother under the CMGF Gaussian approximation (PPDecodeFilter / PP_fixedIntervalSmoother).
  • fit_point_process_emPP_EM equivalent (CMGF E-step + closed-form/Newton M-step, Smith & Brown 2003 PPLDS).
  • fit_hybrid_emmPPCO_EM equivalent (IRLS-pseudo-observation augmented Kalman smoother E-step + closed-form/Newton M-step).
from nstat.extras.em.dynamax_bridge import (
    fit_linear_gaussian_em, fit_point_process_em, fit_hybrid_em,
)

# Poisson point-process state-space EM (PP_EM)
pp = fit_point_process_em(spike_counts, state_dim=2, n_iter=30)
print(pp.observation_matrix, pp.marginal_log_likelihoods[-1])

# Hybrid Poisson + Gaussian observations sharing one latent (mPPCO_EM)
hy = fit_hybrid_em(spike_counts, lfp, state_dim=2, n_iter=30)

Details: em_dynamax.html · Demo: em_dynamax_demo.py

What's planned

v0.4 milestones (see parity/integration_opportunities.md):
  • EM accuracy hardening — replace the moment-matching lag-one cross-covariance approximation in fit_point_process_em / fit_hybrid_em with the exact smoother lag-one covariance, and exact Laplace E[exp(Cx)] correction, to tighten MATLAB-parity from ~1-3% to <1e-4.
  • nstat.extras.spectral.synchrosqueeze — ssqueezepy wrapper, closes the "no synchrosqueeze" admission.
  • nstat.extras.deep_learning — PyTorch decoder bridges (planned).
  • nstat.extras.interop.spikeinterface — spike-sorting-output bridge.

Independence rule

Every extras module is Python-side only. No bridge introduces runtime coupling to the MATLAB cajigaslab/nSTAT repository. The cleanroom-boundary CI gate (tests/test_cleanroom_boundary.py) scans every nstat/extras/**/*.py for the forbidden matlab.engine pattern and fails the build on any match.

License: GPL-2.0 (matches core nSTAT-python). Every extras dep listed above is MIT / BSD-2 / BSD-3 / Apache-2 / GPL-2 — all GPL-2 compatible. The license audit lives in integration_opportunities.md.