Qu

Statistics & estimation

Kalman tracking

a constant-velocity Kalman filter tracking a noisy 1-D position sensor.

catalog/qu_kalman_tracking.qu

State is [position, velocity]; only position is measured (noisily). Each step threads the filter state through `predict` then `update`, Qu's named-return `Model` protocol for the linear Kalman filter (no separate `kalman_predict`/`kalman_update` builtins -- `predict`/`update` dispatch on the model's own `kind`, called via ordinary `state.method(args)` sugar).

What it exercises

How it works

Ground truth: constant velocity plus a noisy position sensor

N = 60
dt = 0.1
true_v = 2.0
t = (0 to N - 1) * dt
true_pos = true_v * t
measured = true_pos + 0.8 * randn(N)

Constant-velocity Kalman filter: state = [position, velocity]

F = [1, dt; 0, 1]
Q = [0.001, 0; 0, 0.001]
H = reshape([1, 0], 1, 2)
R = [0.64]

s = kalman_init([0, 0], [10, 0; 0, 10])
est_pos = zeros(1, N)
est_vel = zeros(1, N)
for k = 0 to N - 1
    s = s.predict(F, Q)
    s = s.update(H, [measured[k]], R)
    est_pos[k] = s.x[0]
    est_vel[k] = s.x[1]
end for

raw_err = rms(measured - true_pos)
filt_err = rms(est_pos - true_pos)
print("Position RMS error: raw sensor={raw_err:.3f}, Kalman estimate={filt_err:.3f}")
print("True velocity={true_v:.2f}, final estimated velocity={est_vel[N-1]:.3f}")

Plot ground truth, raw measurements, and the filtered track

plot(t, true_pos, label="true position", color="black")
plot(t, measured, label="noisy sensor", color="gray")
plot(t, est_pos, label="Kalman estimate", color="blue")
xlabel("Time (s)")
ylabel("Position")
title("Constant-velocity Kalman tracking")
legend()
show plot

Results

printed
Position RMS error: raw sensor=0.832, Kalman estimate=0.215
True velocity=2.00, final estimated velocity=2.023
0 2 4 6 8 10 12 0 1 2 3 4 5 6 Constant-velocity Kalman tracking Time (s) Position true position noisy sensor Kalman estimate
Drawn by running the program above.

The whole program

Runnable as it stands: qu run catalog/qu_kalman_tracking.qu

# qu_kalman_tracking.qu -- a constant-velocity Kalman filter tracking a

# noisy 1-D position sensor.

#

# State is [position, velocity]; only position is measured (noisily). Each

# step threads the filter state through `predict` then `update`, Qu's

# named-return `Model` protocol for the linear Kalman filter (no separate

# `kalman_predict`/`kalman_update` builtins -- `predict`/`update` dispatch

# on the model's own `kind`, called via ordinary `state.method(args)`

# sugar).

#

# Constructs exercised (all real, verified-working builtins):

#   kalman_init(x0, P0)               -- initial state estimate + covariance

#   state.predict(F, Q)               -- F a matrix literal, constant-velocity model

#   state.update(H, z, R)             -- H a 1x2 observation row, z a 1-vector

#   rms                                -- comparing raw vs filtered error


backend auto

#%% Ground truth: constant velocity plus a noisy position sensor

N = 60
dt = 0.1
true_v = 2.0
t = (0 to N - 1) * dt
true_pos = true_v * t
measured = true_pos + 0.8 * randn(N)

#%% Constant-velocity Kalman filter: state = [position, velocity]

F = [1, dt; 0, 1]
Q = [0.001, 0; 0, 0.001]
H = reshape([1, 0], 1, 2)
R = [0.64]

s = kalman_init([0, 0], [10, 0; 0, 10])
est_pos = zeros(1, N)
est_vel = zeros(1, N)
for k = 0 to N - 1
    s = s.predict(F, Q)
    s = s.update(H, [measured[k]], R)
    est_pos[k] = s.x[0]
    est_vel[k] = s.x[1]
end for

raw_err = rms(measured - true_pos)
filt_err = rms(est_pos - true_pos)
print("Position RMS error: raw sensor={raw_err:.3f}, Kalman estimate={filt_err:.3f}")
print("True velocity={true_v:.2f}, final estimated velocity={est_vel[N-1]:.3f}")

#%% Plot ground truth, raw measurements, and the filtered track

plot(t, true_pos, label="true position", color="black")
plot(t, measured, label="noisy sensor", color="gray")
plot(t, est_pos, label="Kalman estimate", color="blue")
xlabel("Time (s)")
ylabel("Position")
title("Constant-velocity Kalman tracking")
legend()
show plot