| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent a775479 commit 51b5298
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -205,8 +205,10 @@ def dlqe(*args, **kwargs): | |||
| 205 | 205 | .. math:: x_e[n+1] = A x_e[n] + B u[n] + L(y[n] - C x_e[n] - D u[n]) | |
| 206 | 206 | ||
| 207 | 207 | 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. | ||
| 210 | 212 | ||
| 211 | 213 | Parameters | |
| 212 | 214 | ---------- | |
@@ -220,20 +222,39 @@ def dlqe(*args, **kwargs): | |||
| 220 | 222 | Set the method used for computing the result. Current methods are | |
| 221 | 223 | 'slycot' and 'scipy'. If set to None (default), try 'slycot' | |
| 222 | 224 | 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}`. | ||
| 223 | 229 | ||
| 224 | 230 | Returns | |
| 225 | 231 | ------- | |
| 226 | 232 | 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. | ||
| 228 | 235 | P : 2D array | |
| 229 | - Solution to Riccati equation. | ||
| 236 | + Steady-state prediction error covariance (prior covariance) that | ||
| 237 | + solves the Riccati equation. | ||
| 230 | 238 | ||
| 231 | 239 | .. math:: | |
| 232 | 240 | ||
| 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 | ||
| 234 | 244 | ||
| 235 | 245 | 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. | ||
| 237 | 258 | ||
| 238 | 259 | Examples | |
| 239 | 260 | -------- | |
@@ -250,8 +271,9 @@ def dlqe(*args, **kwargs): | |||
| 250 | 271 | # Process the arguments and figure out what inputs we received | |
| 251 | 272 | # | |
| 252 | 273 | ||
| 253 | - # Get the method to use (if specified as a keyword) | ||
| 274 | + # Get keywords | ||
| 254 | 275 | method = kwargs.pop('method', None) | |
| 276 | + return_filter_form = kwargs.pop('return_filter_form', False) | ||
| 255 | 277 | if kwargs: | |
| 256 | 278 | raise TypeError("unrecognized keyword(s): ", str(kwargs)) | |
| 257 | 279 | ||
@@ -300,6 +322,9 @@ def dlqe(*args, **kwargs): | |||
| 300 | 322 | # Compute the result (dimension and symmetry checking done in dare()) | |
| 301 | 323 | P, E, LT = dare(A.T, C.T, G @ QN @ G.T, RN, method=method, | |
| 302 | 324 | _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 | ||
| 303 | 328 | return LT.T, P, E | |
| 304 | 329 | ||
| 305 | 330 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -85,6 +85,38 @@ def test_DLQE(method): | |||
| 85 | 85 | L, P, poles = dlqe(A, G, C, QN, RN, method=method) | |
| 86 | 86 | check_DLQE(L, P, poles, G, QN, RN) | |
| 87 | 87 | ||
| 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 | + | ||
| 88 | 120 | def test_lqe_discrete(): | |
| 89 | 121 | """Test overloading of lqe operator for discrete-time systems""" | |
| 90 | 122 | csys = ct.rss(2, 1, 1) | |
@@ -106,6 +138,17 @@ def test_lqe_discrete(): | |||
| 106 | 138 | np.testing.assert_almost_equal(S_lqe, S_dlqe) | |
| 107 | 139 | np.testing.assert_almost_equal(E_lqe, E_dlqe) | |
| 108 | 140 | ||
| 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 | + | ||
| 109 | 152 | # Calling lqe() with no timebase should call lqe() | |
| 110 | 153 | asys = ct.ss(csys.A, csys.B, csys.C, csys.D, dt=None) | |
| 111 | 154 | K_asys, S_asys, E_asys = ct.lqe(asys, Q, R) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -183,9 +183,14 @@ called in several forms: | |||
| 183 | 183 | where :code:`sys` is an :class:`LTI` object, and `A`, `G`, `C`, `QN`, `RN`, | |
| 184 | 184 | and `NN` are 2D arrays of appropriate dimension. If :code:`sys` is a | |
| 185 | 185 | 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` | ||
| 187 | 187 | 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. | ||
| 189 | 194 | ||
| 190 | 195 | .. testsetup:: kalman | |
| 191 | 196 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments