Skip to content

Commit 72262c6

Browse files
sawyerbfullermurrayrm
authored andcommitted
preliminary version of lqe function
* added preliminary version of lqe function, which calculates the gain for a steady-state Kalman filter. * LQE: added python 2 compatibility and unit test
1 parent e2877a1 commit 72262c6

File tree

2 files changed

+91
-2
lines changed

2 files changed

+91
-2
lines changed

control/statefbk.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@
4343
import numpy as np
4444
import scipy as sp
4545
from . import statesp
46+
from .mateqn import care
4647
from .statesp import _ssmatrix
4748
from .exception import ControlSlycot, ControlArgument, ControlDimension
4849

49-
__all__ = ['ctrb', 'obsv', 'gram', 'place', 'place_varga', 'lqr', 'acker']
50+
__all__ = ['ctrb', 'obsv', 'gram', 'place', 'place_varga', 'lqr', 'lqe', 'acker']
5051

5152

5253
# Pole placement
@@ -219,6 +220,76 @@ def place_varga(A, B, p, dtime=False, alpha=None):
219220
# Return the gain matrix, with MATLAB gain convention
220221
return _ssmatrix(-F)
221222

223+
# contributed by Sawyer B. Fuller <minster@uw.edu>
224+
def lqe(A, G, C, QN, RN, NN=None):
225+
"""lqe(A, G, C, QN, RN, [, N])
226+
227+
Linear quadratic estimator design (Kalman filter) for continuous-time
228+
systems. Given the system
229+
230+
Given the system
231+
.. math::
232+
x = Ax + Bu + Gw
233+
y = Cx + Du + v
234+
235+
with unbiased process noise w and measurement noise v with covariances
236+
237+
.. math:: E{ww'} = QN, E{vv'} = RN, E{wv'} = NN
238+
239+
The lqe() function computes the observer gain matrix L such that the
240+
stationary (non-time-varying) Kalman filter
241+
242+
.. math:: x_e = A x_e + B u + L(y - C x_e - D u)
243+
244+
produces a state estimate that x_e that minimizes the expected squared error
245+
using the sensor measurements y. The noise cross-correlation `NN` is set to
246+
zero when omitted.
247+
248+
Parameters
249+
----------
250+
A, G: 2-d array
251+
Dynamics and noise input matrices
252+
QN, RN: 2-d array
253+
Process and sensor noise covariance matrices
254+
NN: 2-d array, optional
255+
Cross covariance matrix
256+
257+
Returns
258+
-------
259+
L: 2D array
260+
Kalman estimator gain
261+
P: 2D array
262+
Solution to Riccati equation
263+
.. math::
264+
A P + P A^T - (P C^T + G N) R^-1 (C P + N^T G^T) + G Q G^T = 0
265+
E: 1D array
266+
Eigenvalues of estimator poles eig(A - L C)
267+
268+
269+
Examples
270+
--------
271+
>>> K, P, E = lqe(A, G, C, QN, RN)
272+
>>> K, P, E = lqe(A, G, C, QN, RN, NN)
273+
274+
See Also
275+
--------
276+
lqr
277+
"""
278+
279+
# TODO: incorporate cross-covariance NN, something like this,
280+
# which doesn't work for some reason
281+
#if NN is None:
282+
# NN = np.zeros(QN.size(0),RN.size(1))
283+
#NG = G @ NN
284+
285+
#LT, P, E = lqr(A.T, C.T, G @ QN @ G.T, RN)
286+
#P, E, LT = care(A.T, C.T, G @ QN @ G.T, RN)
287+
A, G, C = np.array(A, ndmin=2), np.array(G, ndmin=2), np.array(C, ndmin=2)
288+
QN, RN = np.array(QN, ndmin=2), np.array(RN, ndmin=2)
289+
P, E, LT = care(A.T, C.T, np.dot(np.dot(G, QN), G.T), RN)
290+
return _ssmatrix(LT.T), _ssmatrix(P), _ssmatrix(E)
291+
292+
222293
# Contributed by Roberto Bucher <roberto.bucher@supsi.ch>
223294
def acker(A, B, poles):
224295
"""Pole placement using Ackermann method
@@ -307,6 +378,10 @@ def lqr(*args, **keywords):
307378
>>> K, S, E = lqr(sys, Q, R, [N])
308379
>>> K, S, E = lqr(A, B, Q, R, [N])
309380
381+
See Also
382+
--------
383+
lqe
384+
310385
"""
311386

312387
# Make sure that SLICOT is installed

control/tests/statefbk_test.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from __future__ import print_function
77
import unittest
88
import numpy as np
9-
from control.statefbk import ctrb, obsv, place, place_varga, lqr, gram, acker
9+
from control.statefbk import ctrb, obsv, place, place_varga, lqr, lqe, gram, acker
1010
from control.matlab import *
1111
from control.exception import slycot_check, ControlDimension
1212
from control.mateqn import care, dare
@@ -299,6 +299,20 @@ def test_LQR_3args(self):
299299
K, S, poles = lqr(sys, Q, R)
300300
self.check_LQR(K, S, poles, Q, R)
301301

302+
def check_LQE(self, L, P, poles, G, QN, RN):
303+
P_expected = np.array(np.sqrt(G*QN*G * RN))
304+
L_expected = P_expected / RN
305+
poles_expected = np.array([-L_expected], ndmin=2)
306+
np.testing.assert_array_almost_equal(P, P_expected)
307+
np.testing.assert_array_almost_equal(L, L_expected)
308+
np.testing.assert_array_almost_equal(poles, poles_expected)
309+
310+
@unittest.skipIf(not slycot_check(), "slycot not installed")
311+
def test_LQE(self):
312+
A, G, C, QN, RN = 0., .1, 1., 10., 2.
313+
L, P, poles = lqe(A, G, C, QN, RN)
314+
self.check_LQE(L, P, poles, G, QN, RN)
315+
302316
@unittest.skipIf(not slycot_check(), "slycot not installed")
303317
def test_care(self):
304318
#unit test for stabilizing and anti-stabilizing feedbacks

0 commit comments

Comments
 (0)