filterpy's SquareRootKalmanFilter exposes P_post, the posterior state covariance, as a read only
property. Its body is a copy of the P_prior property and reconstructs the covariance from the prior
square root factor _P1_2_prior instead of the posterior factor _P1_2_post. The filter maintains the
posterior factor correctly in update, but P_post never reads it, so the posterior covariance a caller
reads back is the prior covariance, unchanged by the measurement update.
filterpy, filterpy/kalman/square_root.py, current master, the two properties side by side:
@property
def P_prior(self):
""" covariance matrix of the prior"""
return dot(self._P1_2_prior, self._P1_2_prior.T)
@property
def P_post(self):
""" covariance matrix of the posterior"""
return dot(self._P1_2_prior, self._P1_2_prior.T) # copy of P_prior: uses the PRIOR factorupdate maintains the posterior factor (self._P1_2_post = np.copy(self._P1_2)), but P_post
reconstructs from _P1_2_prior, the same factor P_prior uses, so the two properties return the same
matrix.
A square root filter stores the covariance as a factor F with P = F Ft. After a measurement update the
posterior factor differs from the prior factor, so reconstructing from the prior factor returns the prior
covariance:
P_prior:
[[4. 1. ]
[1. 2.5]]
P_post buggy (prior factor):
[[4. 1. ]
[1. 2.5]]
P_post fixed (post factor):
[[0.64 0.24]
[0.24 1.3 ]]
buggy P_post equals P_prior: True
Driving the real library, a predict then a measurement update:
real SquareRootKalmanFilter: P_post == P_prior after update: True (max abs diff 0.0e+00)
KalmanFilter control: P_post == P_prior: False (prior trace 30.2, post trace 6.3133)
master reconstruction on real factors: buggy P_post is the true prior True; fix (posterior factor) is the true posterior True
buggy differs from fix: True; max error buggy vs true posterior 19.1474
On the real filter P_post is byte for byte equal to P_prior after the update. A plain KalmanFilter
on the same problem separates them: its posterior trace 6.31 is well below its prior trace 30.2,
because the measurement reduces uncertainty. The square root filter does maintain the posterior factor;
reconstructing the covariance from it the way master reconstructs the prior, dot(F, F.T), gives exactly
the plain filter's posterior, while the prior factor that P_post actually uses gives the plain filter's
prior. The two differ by up to 19.1.
P_post is a documented public read only attribute of a shipped filter class. The failure is silent: it
returns a finite positive semidefinite matrix, just the wrong one, the prior instead of the posterior, so
any smoother, consistency check, or gating that reads the posterior covariance out of this filter sees a
covariance that never reflects the measurement. It is not floating point round off: P_post and
P_prior are byte identical because they execute the same expression. The fix mirrors the prior
property with the posterior factor: return dot(self._P1_2_post, self._P1_2_post.T).
A note on versions: master reconstructs each covariance as dot(F, F.T), and the numbers above use that
form; the installed 1.4.5 uses the transposed dot(F.T, F) in all of its P properties, so on 1.4.5 the
prior and posterior covariances are themselves transposed relative to the true ones, a separate and older
quirk. The copy of the prior factor into P_post is independent of it: P_post equals P_prior in both
versions, which is what consequence.py shows first, directly on the installed filter.
excerpt.py: the two properties quoted from master with the flags that name the copied factor and the fix.kalman_cov.py: theP = F Ftreconstruction with the buggy prior factor and the correct posterior factor, and the check that the posterior must differ from the prior after an update.consequence.py: the real filter'sP_postequal toP_prior, theKalmanFiltercontrol separating them, and master's reconstruction on the real factors showing the buggyP_postis the true prior and the fix is the true posterior.test_postprior.py: the model's buggy posterior equals the prior, and the real filter reproduces it while the control and the fix are correct.
python kalman_cov.py
python consequence.py
python test_postprior.py
The properties are quoted from current master; the covariances are produced by the real filterpy. The
fix is to reconstruct P_post from _P1_2_post, the posterior factor the filter already maintains.