nstat.extras.validation.pykalman_bridge

Cross-validate nstat.DecodingAlgorithms.kalman_filter() against pykalman.

pykalman (https://github.com/pykalman/pykalman) is a BSD-3 pure-NumPy Kalman implementation, community-resurrected in 2026 (v0.11.2, Jan 2026; 1.3k stars). Because it’s NumPy-only (no JAX), it’s the lowest-friction cross-validation reference for the Gaussian-only Kalman path — perfect for CI parity tests.

This bridge addresses AUDIT D3 (the known smoother-index approximation gap in DecodingAlgorithms.kalman_fixedIntervalSmoother()) by providing an independent reference implementation users can call from the same script that uses nstat’s Kalman primitives.

Install:

pip install nstat-toolbox[test-parity] # or just: pip install pykalman

Functions

cross_validate_kalman(observations, ...[, ...])

Fit a linear-Gaussian Kalman filter (+ smoother) in both nstat and pykalman.

Classes

KalmanComparison(nstat_filtered_means, ...)

Side-by-side Kalman filter / smoother fit comparison.

class nstat.extras.validation.pykalman_bridge.KalmanComparison(nstat_filtered_means, pykalman_filtered_means, nstat_smoothed_means, pykalman_smoothed_means, filtered_inf_norm, smoothed_inf_norm)[source]

Bases: object

Side-by-side Kalman filter / smoother fit comparison.

Variables:
  • nstat_filtered_means (numpy.ndarray) – Filtered state means from DecodingAlgorithms.kalman_filter(), shape (T, Dx).

  • pykalman_filtered_means (numpy.ndarray) – Filtered state means from pykalman.KalmanFilter.filter(), same shape and layout.

  • nstat_smoothed_means (numpy.ndarray | None) – Smoothed state means from DecodingAlgorithms.kalman_fixedIntervalSmoother() if computed (else None), shape (T, Dx).

  • pykalman_smoothed_means (numpy.ndarray | None) – Smoothed state means from pykalman.KalmanFilter.smooth(), same shape (else None).

  • filtered_inf_norm (float) – \(\max_t \|\hat{x}_t^{nstat} - \hat{x}_t^{pykalman}\|_\infty\). Filtered means typically agree to ~1e-8 between any two correct implementations of the same linear-Gaussian model.

  • smoothed_inf_norm (float | None) – Same for smoothed means; reflects the RTS smoother agreement. None if smoothing was not run.

Parameters:
  • nstat_filtered_means (ndarray)

  • pykalman_filtered_means (ndarray)

  • nstat_smoothed_means (ndarray | None)

  • pykalman_smoothed_means (ndarray | None)

  • filtered_inf_norm (float)

  • smoothed_inf_norm (float | None)

nstat_filtered_means: ndarray
pykalman_filtered_means: ndarray
nstat_smoothed_means: ndarray | None
pykalman_smoothed_means: ndarray | None
filtered_inf_norm: float
smoothed_inf_norm: float | None
assert_filtered_agree(atol=0.01)[source]

Assert filtered means agree within atol.

Default atol=1e-2 reflects the current empirical baseline on a 100×2 linear-Gaussian fixture (~2.6e-3, dominated by t=0 initialization convention differences between nstat and pykalman). Tighten to 1e-8 once nstat’s filter is patched to match the pykalman initialization (prior at t=0 vs posterior at t=0). Use this as a regression guard, not as a claim of exact agreement.

Parameters:

atol (float)

Return type:

None

assert_smoothed_agree(atol=0.001)[source]

Assert smoothed means agree within atol.

Default atol=1e-3 reflects the current empirical baseline on a 100×2 linear-Gaussian fixture (~1.6e-4) — dominated by the same t=0 initialization convention difference that drives the filter disagreement (~2.6e-3), since the RTS smoother is forward-filter-then-backward-smooth and inherits the filter’s boundary handling.

Earlier versions of this bridge documented a ~0.4 unit smoother gap and attributed it to “AUDIT D3.” That was a misdiagnosis: the bridge was comparing nstat’s fixed-lag smoother (kalman_fixedIntervalSmoother — Anderson & Moore augmented-state construction) against pykalman’s RTS smoother — fundamentally different algorithms. The fix was to call DecodingAlgorithms.kalman_smoother() (which is a proper RTS pass) and the disagreement collapsed from ~0.4 to ~1.6e-4.

Tighten to 1e-8 once nstat’s filter t=0 init convention is patched.

Parameters:

atol (float)

Return type:

None

nstat.extras.validation.pykalman_bridge.cross_validate_kalman(observations, transition_matrix, observation_matrix, transition_covariance, observation_covariance, initial_state_mean, initial_state_covariance, *, compute_smoother=True)[source]

Fit a linear-Gaussian Kalman filter (+ smoother) in both nstat and pykalman.

Parameters:
  • observations (ndarray) – Observation time series, shape (T, Dy).

  • transition_matrix (ndarray) – State transition matrix \(A\), shape (Dx, Dx).

  • observation_matrix (ndarray) – Observation matrix \(C\), shape (Dy, Dx).

  • transition_covariance (ndarray) – Process noise covariance \(Q\), shape (Dx, Dx).

  • observation_covariance (ndarray) – Measurement noise covariance \(R\), shape (Dy, Dy).

  • initial_state_mean (ndarray) – Initial state estimate \(\hat{x}_0\), shape (Dx,).

  • initial_state_covariance (ndarray) – Initial state covariance \(P_0\), shape (Dx, Dx).

  • compute_smoother (bool) – If True (default), also fit the RTS smoother in both libraries and populate the smoothed-means fields.

Return type:

KalmanComparison

Notes

The bridge calls nstat with the Pythonic keyword signature (observations=, transition=, …) — see DecodingAlgorithms.kalman_filter() for the MATLAB-compatible positional alternative.