FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Add dlqe filter-form gain option · python-control/python-control@51b5298 · GitHub

Commit 51b5298

Browse files
marko1olo
committed
Add dlqe filter-form gain option
1 parent a775479 commit 51b5298

3 files changed

Lines changed: 82 additions & 9 deletions

File tree

‎control/stochsys.py‎

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,10 @@ def dlqe(*args, **kwargs):
205205
.. math:: x_e[n+1] = A x_e[n] + B u[n] + L(y[n] - C x_e[n] - D u[n])
206206
207207
produces a state estimate x_e[n] that minimizes the mean squared
208-
estimation error x[n] - x_e[n] using the sensor measurements y. The
209-
noise cross-correlation `NN` is set to zero when omitted.
208+
estimation error x[n] - x_e[n] using the sensor measurements y. If
209+
`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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,38 @@ 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+
117+
L_pred_false, _, _ = dlqe(A, G, C, QN, RN, method=method, return_filter_form=False)
118+
np.testing.assert_allclose(L_pred, L_pred_false)
119+
88120
def test_lqe_discrete():
89121
"""Test overloading of lqe operator for discrete-time systems"""
90122
csys = ct.rss(2, 1, 1)
@@ -106,6 +138,17 @@ def test_lqe_discrete():
106138
np.testing.assert_almost_equal(S_lqe, S_dlqe)
107139
np.testing.assert_almost_equal(E_lqe, E_dlqe)
108140

141+
# Optional dlqe() keywords should pass through lqe() for DT systems
142+
K_lqe, S_lqe, E_lqe = ct.lqe(dsys, Q, R, return_filter_form=True)
143+
K_dlqe, S_dlqe, E_dlqe = ct.dlqe(dsys, Q, R, return_filter_form=True)
144+
np.testing.assert_almost_equal(K_lqe, K_dlqe)
145+
np.testing.assert_almost_equal(S_lqe, S_dlqe)
146+
np.testing.assert_almost_equal(E_lqe, E_dlqe)
147+
148+
# Calling lqe with return_filter_form=True for a continuous-time system should raise TypeError
149+
with pytest.raises(TypeError, match="unrecognized keyword"):
150+
ct.lqe(csys, Q, R, return_filter_form=True)
151+
109152
# Calling lqe() with no timebase should call lqe()
110153
asys = ct.ss(csys.A, csys.B, csys.C, csys.D, dt=None)
111154
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)

Back | FazBrowse Home | New Git URL