Skip to content

Commit 5a342d7

Browse files
committed
Add dlqe filter-form gain option
1 parent 146ccee commit 5a342d7

3 files changed

Lines changed: 78 additions & 12 deletions

File tree

control/stochsys.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,16 @@ def dlqe(*args, **kwargs):
199199
200200
.. math:: E\{w w^T\} = QN, E\{v v^T\} = RN, E\{w v^T\} = NN
201201
202-
The dlqe() function computes the observer gain matrix L such that the
203-
stationary (non-time-varying) Kalman filter
202+
The dlqe() function computes the one-step predictor gain matrix L such
203+
that the stationary (non-time-varying) Kalman filter
204204
205205
.. math:: x_e[n+1] = A x_e[n] + B u[n] + L(y[n] - C x_e[n] - D u[n])
206206
207-
produces a state estimate x_e[n] that minimizes the expected squared
208-
error using the sensor measurements y. The noise cross-correlation `NN`
209-
is set to zero when omitted.
207+
produces a state estimate x_e[n] whose steady-state estimation error
208+
x[n] - x_e[n] has minimum covariance using the sensor measurements y.
209+
If `return_filter_form` is True, `dlqe` instead returns the filter-form
210+
correction gain whose corresponding predictor gain is `A L`. The noise
211+
cross-correlation `NN` is set to zero when omitted.
210212
211213
Parameters
212214
----------
@@ -220,20 +222,39 @@ def dlqe(*args, **kwargs):
220222
Set the method used for computing the result. Current methods are
221223
'slycot' and 'scipy'. If set to None (default), try 'slycot'
222224
first and then 'scipy'.
225+
return_filter_form : bool, optional
226+
If True, return the Kalman filter gain
227+
:math:`P C^T (C P C^T + R_N)^{-1}` instead of the default predictor
228+
gain :math:`A P C^T (C P C^T + R_N)^{-1}`.
223229
224230
Returns
225231
-------
226232
L : 2D array
227-
Kalman estimator gain.
233+
Kalman estimator gain. By default, this is the predictor-form gain.
234+
If `return_filter_form` is True, this is the filter-form gain.
228235
P : 2D array
229-
Solution to Riccati equation.
236+
Steady-state prediction error covariance (prior covariance) that
237+
solves the Riccati equation.
230238
231239
.. math::
232240
233-
A P + P A^T - (P C^T + G N) R^{-1} (C P + N^T G^T) + G Q G^T = 0
241+
P = A P A^T
242+
- A P C^T (C P C^T + R_N)^{-1} C P A^T
243+
+ G Q_N G^T
234244
235245
E : 1D array
236-
Eigenvalues of estimator poles eig(A - L C).
246+
Eigenvalues of estimator poles. With the default predictor-form gain,
247+
these are `eig(A - L C)`. With `return_filter_form=True`, these are
248+
`eig(A @ (I - L C))`.
249+
250+
Notes
251+
-----
252+
By default, `dlqe` returns the predictor-form gain
253+
:math:`A P C^T (C P C^T + R_N)^{-1}`. Use
254+
`return_filter_form=True` to return the filter-form gain
255+
:math:`P C^T (C P C^T + R_N)^{-1}` instead. The returned covariance
256+
`P` is the steady-state prediction error covariance used in both gain
257+
formulas.
237258
238259
Examples
239260
--------
@@ -250,8 +271,9 @@ def dlqe(*args, **kwargs):
250271
# Process the arguments and figure out what inputs we received
251272
#
252273

253-
# Get the method to use (if specified as a keyword)
274+
# Get keywords
254275
method = kwargs.pop('method', None)
276+
return_filter_form = kwargs.pop('return_filter_form', False)
255277
if kwargs:
256278
raise TypeError("unrecognized keyword(s): ", str(kwargs))
257279

@@ -300,6 +322,9 @@ def dlqe(*args, **kwargs):
300322
# Compute the result (dimension and symmetry checking done in dare())
301323
P, E, LT = dare(A.T, C.T, G @ QN @ G.T, RN, method=method,
302324
_Bs="C", _Qs="QN", _Rs="RN", _Ss="NN")
325+
if return_filter_form:
326+
L = sp.linalg.solve((C @ P @ C.T + RN).T, (P @ C.T).T).T
327+
return L, P, E
303328
return LT.T, P, E
304329

305330

control/tests/stochsys_test.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,35 @@ def test_DLQE(method):
8585
L, P, poles = dlqe(A, G, C, QN, RN, method=method)
8686
check_DLQE(L, P, poles, G, QN, RN)
8787

88+
@pytest.mark.parametrize("method", [None,
89+
pytest.param('slycot', marks=pytest.mark.slycot),
90+
'scipy'])
91+
def test_DLQE_return_filter_form(method):
92+
A = np.array([[0., 1.], [0., 0.5]])
93+
G = np.eye(2)
94+
C = np.array([[1., 0.]])
95+
QN = np.eye(2)
96+
RN = np.array([[1.]])
97+
98+
L_pred, P_pred, E_pred = dlqe(A, G, C, QN, RN, method=method)
99+
L_filter, P_filter, E_filter = dlqe(
100+
A, G, C, QN, RN, method=method, return_filter_form=True)
101+
L_expected = np.linalg.solve(
102+
(C @ P_pred @ C.T + RN).T, (P_pred @ C.T).T).T
103+
104+
assert np.linalg.matrix_rank(A) < A.shape[0]
105+
assert not np.allclose(L_filter, L_pred)
106+
np.testing.assert_allclose(P_filter, P_pred)
107+
np.testing.assert_allclose(E_filter, E_pred)
108+
np.testing.assert_allclose(L_filter, L_expected)
109+
np.testing.assert_allclose(L_pred, A @ L_filter)
110+
np.testing.assert_allclose(
111+
np.sort_complex(E_pred),
112+
np.sort_complex(np.linalg.eigvals(A - L_pred @ C)))
113+
np.testing.assert_allclose(
114+
np.sort_complex(E_filter),
115+
np.sort_complex(np.linalg.eigvals(A @ (np.eye(2) - L_filter @ C))))
116+
88117
def test_lqe_discrete():
89118
"""Test overloading of lqe operator for discrete-time systems"""
90119
csys = ct.rss(2, 1, 1)
@@ -106,6 +135,13 @@ def test_lqe_discrete():
106135
np.testing.assert_almost_equal(S_lqe, S_dlqe)
107136
np.testing.assert_almost_equal(E_lqe, E_dlqe)
108137

138+
# Optional dlqe() keywords should pass through lqe() for DT systems
139+
K_lqe, S_lqe, E_lqe = ct.lqe(dsys, Q, R, return_filter_form=True)
140+
K_dlqe, S_dlqe, E_dlqe = ct.dlqe(dsys, Q, R, return_filter_form=True)
141+
np.testing.assert_almost_equal(K_lqe, K_dlqe)
142+
np.testing.assert_almost_equal(S_lqe, S_dlqe)
143+
np.testing.assert_almost_equal(E_lqe, E_dlqe)
144+
109145
# Calling lqe() with no timebase should call lqe()
110146
asys = ct.ss(csys.A, csys.B, csys.C, csys.D, dt=None)
111147
K_asys, S_asys, E_asys = ct.lqe(asys, Q, R)

doc/stochastic.rst

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,14 @@ called in several forms:
183183
where :code:`sys` is an :class:`LTI` object, and `A`, `G`, `C`, `QN`, `RN`,
184184
and `NN` are 2D arrays of appropriate dimension. If :code:`sys` is a
185185
discrete-time system, the first two forms will compute the discrete
186-
time optimal controller. For the second two forms, the :func:`dlqr`
186+
time optimal estimator. For the second two forms, the :func:`dlqe`
187187
function can be used. Additional arguments and details are given on
188-
the :func:`lqr` and :func:`dlqr` documentation pages.
188+
the :func:`lqe` and :func:`dlqe` documentation pages.
189+
190+
For discrete-time systems, :func:`dlqe` returns the predictor-form gain
191+
:math:`A P C^T (C P C^T + R_N)^{-1}` by default. Use
192+
``return_filter_form=True`` to return the filter-form gain
193+
:math:`P C^T (C P C^T + R_N)^{-1}` instead.
189194

190195
.. testsetup:: kalman
191196

0 commit comments

Comments
 (0)