Filtering and State Estimation
This guide covers the filtering algorithms available in the library.
Kalman Filter Family
Linear Kalman Filter
The standard Kalman filter is optimal for linear-Gaussian systems:
Prediction step:
import numpy as np
from pytcl.dynamic_estimation import kf_predict
# Constant-velocity state [position, velocity]
x = np.array([0.0, 1.0])
P = np.eye(2) * 0.5
F = np.array([[1.0, 0.1], [0.0, 1.0]])
Q = np.eye(2) * 0.01
prediction = kf_predict(x, P, F, Q)
x_pred = prediction.x
P_pred = prediction.P
Update step:
from pytcl.dynamic_estimation import kf_update
# z: measurement, H: measurement matrix, R: measurement noise
z = np.array([0.12])
H = np.array([[1.0, 0.0]])
R = np.array([[0.1]])
update = kf_update(x_pred, P_pred, z, H, R)
x_upd = update.x
P_upd = update.P
kf_update returns a NamedTuple with six fields: x (updated state),
P (updated covariance), y (innovation), S (innovation
covariance), K (Kalman gain), and likelihood (measurement
likelihood).
Combined predict-update:
from pytcl.dynamic_estimation import kf_predict_update
update = kf_predict_update(x, P, z, F, Q, H, R)
Extended Kalman Filter (EKF)
For nonlinear systems, the EKF linearizes around the current estimate.
ekf_predict and ekf_update take the Jacobian evaluated at the
linearization point (a matrix, not a callable):
from pytcl.dynamic_estimation import ekf_predict, ekf_update
# Define nonlinear dynamics and Jacobian
def f(x):
# Nonlinear state transition
return np.array([x[0] + x[1], x[1] * 0.99])
def F_jacobian(x):
return np.array([[1, 1], [0, 0.99]])
# Predict (Jacobian evaluated at the current estimate)
pred = ekf_predict(x, P, f, F_jacobian(x), Q)
# Define nonlinear measurement and Jacobian
def h(x):
# Range measurement
return np.array([np.sqrt(x[0]**2 + x[1]**2)])
def H_jacobian(x):
r = np.sqrt(x[0]**2 + x[1]**2)
return np.array([[x[0]/r, x[1]/r]])
# Update (Jacobian evaluated at the predicted state)
upd = ekf_update(pred.x, pred.P, z, h, H_jacobian(pred.x), R)
Automatic Jacobian computation:
from pytcl.dynamic_estimation import ekf_predict_auto, ekf_update_auto
# Uses numerical differentiation
pred = ekf_predict_auto(x, P, f, Q)
upd = ekf_update_auto(pred.x, pred.P, z, h, R)
Unscented Kalman Filter (UKF)
The UKF uses sigma points to capture the mean and covariance through nonlinear transformations:
from pytcl.dynamic_estimation import ukf_predict, ukf_update
# No Jacobians needed!
pred = ukf_predict(x, P, f, Q)
upd = ukf_update(pred.x, pred.P, z, h, R)
Cubature Kalman Filter (CKF)
The CKF uses spherical-radial cubature points, providing a good balance between accuracy and computational cost:
from pytcl.dynamic_estimation import ckf_predict, ckf_update
pred = ckf_predict(x, P, f, Q)
upd = ckf_update(pred.x, pred.P, z, h, R)
Particle Filters
For non-Gaussian or highly nonlinear systems, particle filters provide a flexible Monte Carlo approach:
from pytcl.dynamic_estimation import (
initialize_particles,
bootstrap_pf_step,
particle_mean,
particle_covariance,
)
# Initialize particles from prior
x0 = np.array([0.0, 1.0])
P0 = np.eye(2) * 0.5
state = initialize_particles(x0, P0, N=1000)
# Define process noise sampler
def Q_sample(N, rng):
return rng.multivariate_normal(np.zeros(2), Q, size=N)
# Run filter step
state = bootstrap_pf_step(
state.particles, state.weights,
z, f, h, Q_sample, R,
resample_method="systematic"
)
# Extract estimates
x_est = particle_mean(state.particles, state.weights)
P_est = particle_covariance(state.particles, state.weights)
Smoothing
The library provides RTS (Rauch-Tung-Striebel) smoothing for obtaining
optimal estimates using future measurements. kf_smooth performs one
backward step at a time, so run it in a loop over the stored forward
filter results:
from pytcl.dynamic_estimation import kf_smooth
# Forward pass: store filtered and predicted results
zs = [np.array([0.1]), np.array([0.25]), np.array([0.4])]
x_filt, P_filt = [], []
x_pred_list, P_pred_list = [], []
xk, Pk = x0, P0
for z_k in zs:
pred = kf_predict(xk, Pk, F, Q)
x_pred_list.append(pred.x)
P_pred_list.append(pred.P)
upd = kf_update(pred.x, pred.P, z_k, H, R)
xk, Pk = upd.x, upd.P
x_filt.append(xk)
P_filt.append(Pk)
# Backward pass: one RTS step per time index
n = len(zs)
x_smooth = [None] * n
P_smooth = [None] * n
x_smooth[-1], P_smooth[-1] = x_filt[-1], P_filt[-1]
for k in range(n - 2, -1, -1):
x_smooth[k], P_smooth[k] = kf_smooth(
x_filt[k], P_filt[k],
x_pred_list[k + 1], P_pred_list[k + 1],
x_smooth[k + 1], P_smooth[k + 1],
F,
)
Information Filter
The information filter is the dual of the Kalman filter, working with the information matrix (inverse covariance):
from pytcl.dynamic_estimation import (
information_filter_predict,
information_filter_update,
)
# Work with information form: y = P^{-1} x, Y = P^{-1}
Y = np.linalg.inv(P)
y = Y @ x
y_pred, Y_pred = information_filter_predict(y, Y, F, Q)
y_upd, Y_upd = information_filter_update(y_pred, Y_pred, z, H, R)
Square-Root Kalman Filters
Square-root filters propagate the Cholesky factor of the covariance matrix instead of the covariance itself. This provides improved numerical stability and guarantees positive semi-definiteness.
Square-Root Kalman Filter (SRKF)
from pytcl.dynamic_estimation import srkf_predict, srkf_update
import numpy as np
# Initialize with Cholesky factors instead of covariances
x = np.array([0.0, 1.0, 0.0, 0.5])
P = np.eye(4) * 0.1
S = np.linalg.cholesky(P) # S @ S.T = P
# System matrices
F = np.array([[1, 0.1, 0, 0], [0, 1, 0, 0],
[0, 0, 1, 0.1], [0, 0, 0, 1]])
Q = np.eye(4) * 0.01
S_Q = np.linalg.cholesky(Q)
# Prediction
pred = srkf_predict(x, S, F, S_Q)
x_pred, S_pred = pred.x, pred.S
# Update
z = np.array([0.1, 0.05])
H = np.array([[1, 0, 0, 0], [0, 0, 1, 0]])
R = np.eye(2) * 0.1
S_R = np.linalg.cholesky(R)
upd = srkf_update(x_pred, S_pred, z, H, S_R)
x_upd, S_upd = upd.x, upd.S
# Reconstruct covariance if needed
P_upd = S_upd @ S_upd.T
U-D Factorization Filter
Bierman’s U-D filter uses a different factorization: P = U @ D @ U.T
where U is unit upper triangular and D is diagonal.
from pytcl.dynamic_estimation import (
ud_factorize, ud_reconstruct,
ud_predict, ud_update
)
# Factorize initial covariance
P = np.diag([0.5, 1.0, 0.5, 1.0])
U, D = ud_factorize(P)
# Prediction
x_pred, U_pred, D_pred = ud_predict(x, U, D, F, Q)
# Update
x_upd, U_upd, D_upd, innovation, likelihood = ud_update(
x_pred, U_pred, D_pred, z, H, R
)
# Reconstruct covariance if needed
P_upd = ud_reconstruct(U_upd, D_upd)
Square-Root UKF
The square-root UKF combines the benefits of the UKF (no Jacobians needed) with numerical stability of square-root formulations.
from pytcl.dynamic_estimation import sr_ukf_predict, sr_ukf_update
# Nonlinear state transition
def f(x):
return np.array([x[0] + x[1] * 0.1, x[1] * 0.99])
# Nonlinear measurement
def h(x):
return np.array([np.sqrt(x[0]**2 + x[1]**2)])
# Two-state setup with Cholesky factors
x = np.array([0.0, 1.0])
S = np.linalg.cholesky(np.eye(2) * 0.5)
S_Q = np.linalg.cholesky(np.eye(2) * 0.01)
S_R = np.linalg.cholesky(np.array([[0.1]]))
z = np.array([0.9])
# Predict and update
pred = sr_ukf_predict(x, S, f, S_Q)
upd = sr_ukf_update(pred.x, pred.S, z, h, S_R)
Interacting Multiple Model (IMM) Estimator
The IMM estimator handles systems that can switch between multiple dynamic models. Each model represents a different motion mode (e.g., constant velocity vs. maneuvering).
Basic IMM Usage
from pytcl.dynamic_estimation import imm_predict, imm_update
from pytcl.dynamic_models import f_coord_turn_2d
# Two modes: constant velocity (CV) and coordinated turn (CT)
x = np.array([0.0, 10.0, 0.0, 5.0]) # [x, vx, y, vy]
P = np.eye(4) * 1.0
# Mode probabilities and transition matrix
mu = np.array([0.9, 0.1]) # Start in CV mode
Pi = np.array([[0.95, 0.05], # CV -> CV, CV -> CT
[0.10, 0.90]]) # CT -> CV, CT -> CT
# Model-specific dynamics
dt = 0.1
F_cv = np.array([[1, dt, 0, 0], [0, 1, 0, 0],
[0, 0, 1, dt], [0, 0, 0, 1]])
F_ct = f_coord_turn_2d(T=dt, omega=0.2) # Coordinated turn model
Q_cv = np.eye(4) * 0.01
Q_ct = np.eye(4) * 0.1 # Higher uncertainty for maneuvering
# Predict
pred = imm_predict(
[x, x], [P, P], mu, Pi,
[F_cv, F_ct], [Q_cv, Q_ct]
)
# Update
z = np.array([0.5, 0.3])
H = np.array([[1, 0, 0, 0], [0, 0, 1, 0]])
R = np.eye(2) * 0.1
upd = imm_update(
pred.mode_states, pred.mode_covs, pred.mode_probs,
z, [H, H], [R, R]
)
# Combined estimate
x_est = upd.x
P_est = upd.P
mode_probs = upd.mode_probs # Current mode probabilities
IMMEstimator Class
For stateful IMM filtering:
from pytcl.dynamic_estimation import IMMEstimator
# Initialize
Pi = np.array([[0.95, 0.05], [0.10, 0.90]])
imm = IMMEstimator(n_modes=2, state_dim=4, transition_matrix=Pi)
imm.initialize(x, P)
imm.set_mode_model(0, F_cv, Q_cv)
imm.set_mode_model(1, F_ct, Q_ct)
imm.set_measurement_model(H, R)
# Filter loop
measurements = [np.array([1.0, 0.5]), np.array([2.1, 1.1])]
for z in measurements:
result = imm.predict_update(z)
print(f"State: {result.x}, Mode probs: {imm.mode_probs}")
See Also
Advanced Filtering Topics:
Constrained State Estimation – Apply state constraints during filtering (geofenced tracking, bounded velocities)
Hybrid Linear/Nonlinear Filtering with RBPF – Hybrid particle/Kalman filtering for mixed linear/nonlinear systems
Adaptive Filtering – Adaptive noise tuning and filter parameters
Information Filters and SRIF – Information representation and filter variants
Particle Filters & Non-Gaussian Estimation – Bootstrap and advanced particle filtering techniques
Related Guides:
Kalman Filter Tuning Guide – Tuning filter parameters and covariance
Smoothing Algorithms & Offline Estimation – Batch estimation and smoothing algorithms