Skip to main content

Pairwise Kalman Filter (PKF) and variants (EPKF, UPKF, PPF) and pairwise smoothers for linear and nonlinear state estimation

Project description

AwesomePKF

This repository contains a set of programs illustrating the Pairwise Kalman Filter (PKF), a generalization of the classical Kalman Filter, extended to non-linear models. It includes several variants of non-linear filters:

  • Extended Pairwise Kalman Filter (EPKF)
  • Unscented Pairwise Kalman Filter (UPKF), with multiple variants depending on the choice of sigma points
  • Pairwise Particle Filter (PPF)

together with the classic (non-pairwise) Unscented Kalman Filter (UKF) and Particle Filter (PF) as baselines.

For offline post-processing it provides the Linear Pairwise Kalman Smoother (PKS) — a backward pass on the joint (X, Y) Markov chain, available in six equivalent variants selectable via method=: RTS (default), BF (Bryson-Frazier), MBF (Modified Bryson-Frazier), MF (Mayne-Fraser two-filter), DWY (Desai-Weinert-Yusypchuk backward filter) and VAR (variational / lifted block-tridiagonal). All return the same estimate on the linear-Gaussian model (Geng et al., 2023); VAR additionally exposes the lag-one cross-covariance Mk_smooth.


Table of Contents


Installation

From PyPI (recommended)

pip install awesomepkf

From source

git clone https://github.com/SDerrode/awesomepkf.git
cd awesomepkf
pip install .

Development install

pip install -e ".[dev]"

Requirements

  • Python >= 3.10
  • numpy, scipy, matplotlib, pandas, rich, chardet, sympy (plus tomli on Python < 3.11)

Quick Start

from prg.classes.linear_pkf import Linear_PKF
from prg.models.linear import ModelFactoryLinear

model = ModelFactoryLinear.create("model_x1_y1_AQ_pairwise")
pkf = Linear_PKF(model)
# ... run the filter step by step

Or use the CLI entry points installed with the package:

awesomepkf-simulate --N 2000 --linear-model-name "model_x1_y1_AQ_pairwise" --data-filename "testL.csv" --s-key 303
awesomepkf-pkf      --linear-model-name "model_x1_y1_AQ_pairwise" --data-filename "testL.csv" --plot

Tutorials

Interactive Jupyter notebooks are available in the notebooks/ directory:

# Notebook Description
01 tutorial_01_getting_started.ipynb Introduction to the PKF framework: linear models, running the filter, visualizing estimates, error metrics (MSE, NEES, NIS), comparing PKF / EPKF / UPKF
02 tutorial_02_nonlinear_models.ipynb Nonlinear models: EPKF, UPKF, PPF and PF — classic vs pairwise, sigma-point sets, particle count impact, filter comparison
03 tutorial_03_sigma_points.ipynb Sigma-point sets for the UPKF: wan2000, cpkf, lerner2002, ito2000 — impact on estimation accuracy
04 tutorial_04_particle_filters.ipynb Particle filters (PPF and PF): tuning the number of particles, resampling, comparison with EPKF/UPKF
05 tutorial_05_new_model_lotkavolterra.ipynb How to add a new nonlinear pairwise model: Lotka-Volterra prey-predator (dim_x=1, dim_y=1), augmented version, filtering with EPKF/UPKF/PPF
06 tutorial_06_filter_runner_and_config.ipynb High-level orchestration with FilterRunner and RunOptions; parameter sweeps via model_kwargs; saving and replaying experiments through a reproducible JSON spec
07 tutorial_07_smoothers.ipynb Pairwise smoothing — backward recursion and forward-filtering / backward-smoothing; ±2σ envelope shrinkage, the Joseph form, and a decision rule for choosing among the available smoothers
08 tutorial_08_real_data_pkf_learning.ipynb Estimating the 1D linear PMM parameters (a, b, c, d, e) from a real two-column time series (wind-farm active power vs wind speed); PMM vs HMM/Kalman projection; converting to LinearAmQ kwargs
09 tutorial_09_linear_smoothers.ipynb The six linear pairwise smoothers behind one façade Linear_PKS(param, method=...) — RTS, BF, MBF, MF, DWY and the variational/lifted VAR form; shows all six return the same smoothed mean/covariance to round-off (~1e-15), companion to the python -m prg.run_dwy_equivalence non-regression check

Parameter learning from data

The prg.learning module offers two estimators for the linear-Gaussian case: a method-of-moments fit of the scalar PMM, and a partial EM for the joint noise covariance.

Method of moments (scalar PMM)

For the linear, scalar case (dim_x = dim_y = 1), it estimates the five PMM parameters (a, b, c, d, e) from a two-column time series by the method of moments.

awesomepkf-fit-pkf \
    --data-filename data/samples/windfarms/site1_202210_Month_586_norm.csv \
    --x-col ActivePower_KWh --y-col WindSpeed \
    --output learned_params.npz --verbose 1

On the embedded WindFarms series the fit lands clearly outside the HMM (classical Kalman) submanifold — the off-HMM gap Δc ≈ 0.53 between the estimated c and its HMM projection a·b² means a pairwise model tracks the state more tightly than the classical KF. See tutorial_08 for the full load → fit → compare → convert workflow.

A small WindFarms sample is shipped under data/samples/; the full dataset (BuildingTemp, SeattleTemp, multiple WindFarms sites and granularities) is kept outside the repository — point --data-filename at a local copy if needed.

Partial EM for the noise covariance (linear-Gaussian)

For a linear-Gaussian pairwise couple Z = (X, Y) with the transition A known, prg.learning.estimate_noise_em runs a partial EM that estimates only the joint process-noise covariance Q (with B = I, so Q is the effective process covariance). The E-step uses the variational linear smoother (method="VAR", the only backward pass exposing the lag-one cross-covariance Mk_smooth); the M-step is a unique closed form given A, and the observed-data log-likelihood it returns is monotone non-decreasing.

from prg.learning import estimate_noise_em

result = estimate_noise_em(param, data, block_diagonal=True)
result.Q          # estimated (dim_xy, dim_xy) joint noise covariance, PSD
result.loglik     # per-iteration observed-data log-likelihood
result.converged  # bool

Identifiability. Fixing A removes the (A, Q) gauge freedom, but the full joint Q is still not identifiable from the hidden state: the cross-noise block Q_xy lies on a near-flat likelihood ridge and the EM estimate of Q_xy tracks the initialisation rather than converging to the truth as N grows. The X- and Y-noise blocks remain well identified, so pass block_diagonal=True to fit the well-conditioned block-diagonal sub-model (Q_xy = 0), which recovers cleanly at modest N. This is a library function (returning an EMNoiseResult) — unlike the PMM estimator it has no CLI.

Parameter identification for nonlinear EPKF/UPKF models is not covered by this estimator — those require a separate procedure (e.g. a neural network).


Models and Simulations

The repository provides a program called run_simulator.py to simulate data according to linear and non-linear models.


Filters

Each filter is exposed through a single run_<filter>.py script (a thin wrapper over prg.run_filter). The same script does both jobs: it simulates and filters when given --N, or filters a previously saved file when given --data-filename (the two are mutually exclusive).

Pairwise Kalman Filter (PKF)

  • run_linear_pkf.py – filter linear data either from simulated data or from a previously saved file (e.g., generated with run_simulator.py)

Extended Pairwise Kalman Filter (EPKF)

  • run_nonlinear_epkf.py – filter non-linear data either from simulated data or from a previously saved file (e.g., generated with run_simulator.py)

Unscented Pairwise Kalman Filter (UPKF)

  • run_nonlinear_upkf.py – filter non-linear data either from simulated data or from a previously saved file (e.g., generated with run_simulator.py)

Pairwise Particle Filter (PPF)

  • run_nonlinear_ppf.py – filter non-linear data either from simulated data or from a previously saved file (e.g., generated with run_simulator.py)

Unscented Kalman Filter (UKF)

  • run_nonlinear_ukf.py – the classic (non-pairwise) sigma-point filter for models Markov in X alone; takes --sigma-set.

Particle Filter (PF)

  • run_nonlinear_pf.py – the classic (non-pairwise) bootstrap particle filter; takes --n-particles.

Smoothers

Smoothers are two-pass, offline estimators that condition on the entire observation sequence y_{1:N}. They produce posterior means and covariances p(X_n | y_{1:N}) that are at least as good (in PSD sense) as the corresponding forward filter outputs p(X_n | y_{1:n}).

Linear Pairwise Kalman Smoother (PKS)

The linear PKS runs the PKF forward, then a backward Rauch-Tung-Striebel recursion at the joint (X, Y) level. The pairwise model is Markov in Z = (X, Y) (not in X alone), so the smoothing gain G_n has shape (dim_x, dim_x + dim_y). Equivalently, the linear PKS is the classical RTS smoother applied to the augmented state Z' = (X, Y) with degenerate observation Y_n = (0, I) Z'_n (R^aug = 0).

Linear_PKS is a façade that selects the backward pass via method= — one of six: "RTS" (default), "BF" (Bryson-Frazier), "MBF" (Modified Bryson-Frazier), "MF" (Mayne-Fraser two-filter), "DWY" (Desai-Weinert-Yusypchuk backward filter) and "VAR" (variational / lifted). All return the same smoothed mean and covariance as RTS to machine precision on the linear-Gaussian model (cf. Geng et al., 2023; verified by test_variant_equals_rts / test_dwy_equals_rts), and the matching explicit classes Linear_PKS_RTS, Linear_PKS_BF, Linear_PKS_MBF, Linear_PKS_MF, Linear_PKS_DWY, Linear_PKS_VAR are all exported. VAR solves for the whole smoothed trajectory as a single block-tridiagonal linear system (lifted / quadratic-program form, requiring full-rank process noise R = B Q Bᵀ ≻ 0) and is the only variant that also writes the lag-one cross-covariance Mk_smooth[n] = Cov(X_{n+1}, X_n | y_{0:N}) — the cross-moment the EM noise estimator consumes.

The six backward passes (all write Xkp1_smooth / PXXkp1_smooth; only VAR also writes Mk_smooth):

method= Smoother Backward pass
"RTS" (default) Rauch–Tung–Striebel classical forward–backward smoothing gain G_n (shape dim_x × dim_xy) coupling X_n to the whole couple Z_{n+1}
"BF" Bryson–Frazier adjoint smoother at the couple level: propagates the predicted-couple adjoint (μ_n, N_n)
"MBF" Modified Bryson–Frazier (Bierman) adjoint smoother at the filtered X level (dim_x), via the adjoint pair (λ_n, Λ_n)
"MF" Mayne–Fraser (two-filter) fuses, in information form, the forward posterior with a backward information filter
"DWY" Desai–Weinert–Yusypchuk dual of RTS: a backward pairwise filter on the time-reversed (complementary) couple model
"VAR" Variational / lifted one block-tridiagonal solve (quadratic program in the latent trajectory); the only variant exposing Mk_smooth
from prg.classes.linear_pks import Linear_PKS
from prg.classes.param_linear import ParamLinear
from prg.models.linear import ModelFactoryLinear

model  = ModelFactoryLinear.create("model_x1_y1_AQ_pairwise")
params = model.get_params().copy()
dim_x  = params.pop("dim_x");  dim_y = params.pop("dim_y")
param  = ParamLinear(0, dim_x, dim_y, **params)

pks = Linear_PKS(param, sKey=42, joseph=False)
# joseph=True selects the explicitly PSD-preserving Joseph form
# (mathematically equivalent at the optimal gain, useful for ill-conditioned cases).

results = pks.process_N_data_smoother(N=500)
# each tuple: (k, x_true, y_obs, X_predict, X_update, X_smooth)

Implementation: prg/classes/linear_pks.py. Tests: prg/tests/test_linear_pks.py (62 tests, including PSD shrinkage, Joseph equivalence, augmented-state RTS equivalence, the BF/MBF/MF/DWY/VAR ≡ RTS equivalence, the VAR lag-one cross-covariance, and full exception/logging coverage).

Deterministic control (consigne)

All six variants accept an optional known control input u_n: the couple obeys Z_{n+1} = A Z_n + G u_n + B W_n with a control matrix G (shape (dim_xy, dim_u)). Pass G to ParamLinear(..., G=G) and the control sequence u to simulate_N_data(N, u=...) and process_N_data_smoother(N, ..., u=...). A deterministic control shifts only the means (never the covariances), so it is applied by exact mean-trajectory superposition and behaves identically across all six backward passes (which stay equivalent to ~1e-15). u=None / G=None keeps the autonomous model — fully backward compatible.

import numpy as np
G   = np.array([[0.6], [0.3]])                    # control on the (X, Y) couple
param = ParamLinear(0, dim_x, dim_y, **params, G=G)
sim = Linear_PKF(param, sKey=42).simulate_N_data(N, u=u)
res = Linear_PKS(param, method="VAR").process_N_data_smoother(
    N=None, data_generator=iter(sim), u=u)

Tests: prg/tests/test_linear_pks_control.py (4 tests).


Paper Reproducibility Scripts

The following scripts reproduce all figures and tables from the article "Non-linear extensions to Gaussian pairwise Kalman filter". Each script can be run independently from the repository root.

Section 4 — Simulation Results

Script Figures generated
run_paper_section4.py epkf_observations_x1_y1_Retroactions.png, epkf_x1_y1_Retroactions.png, upkf_x1_y1_Retroactions.png, ppf_x1_y1_Retroactions.png + Tables 1 & 2
run_paper_section4_backaction.py backaction_mse_nees_vs_b.png
run_paper_section4_multip.py multip_mse_nees_vs_sigma.png
run_paper_section4_sensitivity.py console output — mean ± std of MSE over 30 seeds
python3 -m prg.run_paper_section4
python3 -m prg.run_paper_section4_backaction
python3 -m prg.run_paper_section4_multip
python3 -m prg.run_paper_section4_sensitivity

Section 5 — Real Data Experiment (S&P 500 Stochastic Volatility)

Script Figures generated
run_paper_section5.py nn_gx_gy_sv.png, epkf_sv.png, upkf_sv.png, ppf_sv.png
run_paper_section5_enso.py archived ENSO experiment (Niño 3.4 / SOI), kept for reference
python3 -m prg.run_paper_section5       # requires: pip install yfinance
python3 -m prg.run_paper_section5_enso  # archived version

Note: all figures are saved in papier_NonLinearPKF/figures/.


Usage Examples

Simulate Linear Data and Filter with PKF

awesomepkf-simulate --N 2000 --linear-model-name "model_x1_y1_AQ_pairwise" --data-filename "testL.csv" --verbose 1 --s-key 303
awesomepkf-pkf      --linear-model-name "model_x1_y1_AQ_pairwise" --data-filename "testL.csv" --verbose 1 --save-history --plot

Simulate Non-Linear Data and Filter with EPKF, UPKF and PPF

awesomepkf-simulate --N 1000 --nonlinear-model-name "model_x2_y1_pairwise" --data-filename "testNL.csv" --verbose 1 --s-key 303

awesomepkf-epkf --nonlinear-model-name "model_x2_y1_pairwise" --data-filename "testNL.csv"                      --verbose 1 --save-history --plot
awesomepkf-upkf --nonlinear-model-name "model_x2_y1_pairwise" --data-filename "testNL.csv" --sigma-set "wan2000"  --verbose 1 --save-history --plot
awesomepkf-ppf  --nonlinear-model-name "model_x2_y1_pairwise" --data-filename "testNL.csv" --n-particles 300      --verbose 1 --save-history --plot

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

awesomepkf-2.13.0.tar.gz (190.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

awesomepkf-2.13.0-py3-none-any.whl (247.3 kB view details)

Uploaded Python 3

File details

Details for the file awesomepkf-2.13.0.tar.gz.

File metadata

  • Download URL: awesomepkf-2.13.0.tar.gz
  • Upload date:
  • Size: 190.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for awesomepkf-2.13.0.tar.gz
Algorithm Hash digest
SHA256 128f22b8ea5aa970e186e342a3005a69569b3279fdea5bd7b843dac8ef9b69ef
MD5 07c0c925615e012b012482936152f863
BLAKE2b-256 7576f5afb31e47595a5847679729f28ce43e237af62ac4682da5803725f88200

See more details on using hashes here.

Provenance

The following attestation bundles were made for awesomepkf-2.13.0.tar.gz:

Publisher: publish.yml on SDerrode/awesomepkf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file awesomepkf-2.13.0-py3-none-any.whl.

File metadata

  • Download URL: awesomepkf-2.13.0-py3-none-any.whl
  • Upload date:
  • Size: 247.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for awesomepkf-2.13.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b64a9df8282a61e36481b4ee7be7df371e5660f44986780e9c1f8e08d288ebe9
MD5 9924d6eadf4f0369f43382dca7aa2759
BLAKE2b-256 beb5283edad33cd16db143d2c919e8919d2b7ad923e536b1edb04537415e95b2

See more details on using hashes here.

Provenance

The following attestation bundles were made for awesomepkf-2.13.0-py3-none-any.whl:

Publisher: publish.yml on SDerrode/awesomepkf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page