Skip to content

Auto-Tuning

Automatically tune process noise (\(Q\)) and measurement noise (\(R\)) covariances from data using NIS-based optimization.

Available Methods

Iterative Tuning

Gradient-descent style optimization that adjusts \(Q\) and \(R\) until the mean NIS matches the expected value:

import numpy as np
from kalbee import tune_kalman_filter

# measurements: (T, m) array
# F: transition matrix, H: measurement matrix
result = tune_kalman_filter(
    measurements=measurements,
    F=F, H=H,
    n_iter=50,
    learning_rate=0.01,
    target_nis_ratio=1.0,  # mean_NIS / m should be 1.0
    tol=1e-4,
)

print(f"Converged: {result.converged}")
print(f"Iterations: {result.iterations}")
print(f"Q:\n{result.Q}")
print(f"R:\n{result.R}")
print(f"NIS history: {result.nis_history}")

Parameters:

Parameter Default Description
Q_init 0.01 * I Initial process noise
R_init I Initial measurement noise
n_iter 50 Maximum iterations
learning_rate 0.01 Step size for updates
target_nis_ratio 1.0 Target: mean_NIS / m
tol 1e-4 Convergence tolerance

Quick Tuning

Single-pass tuning for fast results:

from kalbee import quick_tune

Q, R = quick_tune(
    measurements=measurements,
    F=F, H=H,
    process_var=0.01,
    measurement_var=1.0,
)

print(f"Q:\n{Q}")
print(f"R:\n{R}")

Complete Example

import numpy as np
from kalbee import KalmanFilter, tune_kalman_filter, quick_tune

# Generate synthetic data
np.random.seed(42)
true_Q = np.eye(2) * 0.05
true_R = np.array([[0.8]])

F = np.array([[1, 1], [0, 1]])
H = np.array([[1, 0]])

# True state trajectory
T = 200
true_states = np.zeros((T, 2, 1))
for k in range(1, T):
    true_states[k] = F @ true_states[k-1] + np.random.multivariate_normal(
        np.zeros(2), true_Q
    ).reshape(2, 1)

# Noisy measurements
measurements = np.array([
    H @ s + np.random.multivariate_normal(np.zeros(1), true_R).reshape(1, 1)
    for s in true_states
]).squeeze(-1)  # (T, 1)

# Method 1: Iterative tuning
result = tune_kalman_filter(measurements, F, H, n_iter=50)
print(f"Iterative: Q diag = {np.diag(result.Q)}, R = {result.R[0,0]:.3f}")

# Method 2: Quick tuning
Q_quick, R_quick = quick_tune(measurements, F, H)
print(f"Quick:     Q diag = {np.diag(Q_quick)}, R = {R_quick[0,0]:.3f}")

# Compare with ground truth
print(f"True:      Q diag = {np.diag(true_Q)}, R = {true_R[0,0]:.3f}")

When to Use

✅ Use auto-tuning when ❌ Don't use when
Q/R are unknown You have accurate noise models
You have enough measurement data Very short data sequences
Filter performance is poor Filter is already well-tuned
Rapid prototyping Production systems with known physics