@@ -59,7 +59,9 @@ def test_lqe_symmetric_kwargs():
5959 symmetric_kwargs = {"rtol" : 1e-12 },
6060 )
6161
62- def test_dlqe_symmetric_kwargs ():
62+ @pytest .mark .parametrize ("return_filter_form" , [False , True ])
63+ @pytest .mark .parametrize ("cdlqe" , [lqe , dlqe ])
64+ def test_dlqe_symmetric_kwargs (cdlqe , return_filter_form ):
6365 A = np .array ([
6466 [0.5 , 0. ],
6567 [0. , 0.4 ]
@@ -71,14 +73,16 @@ def test_dlqe_symmetric_kwargs():
7173 [0.2 + 1e-15 , 1. ]
7274 ])
7375 RN = np .eye (2 )
76+ sys = ct .ss (A , G , C , 0 , dt = True )
7477
7578 # Exact symmetry check should fail
7679 with pytest .raises (ControlArgument , match = "symmetric" ):
77- dlqe (A , G , C , QN , RN , method = "scipy" )
80+ cdlqe (sys , QN , RN , method = "scipy" ,
81+ return_filter_form = return_filter_form )
7882
7983 # Passing rtol should allow the nearly symmetric matrix
80- dlqe (
81- A , G , C , QN , RN ,
84+ cdlqe (
85+ sys , QN , RN , return_filter_form = return_filter_form ,
8286 method = "scipy" ,
8387 symmetric_kwargs = {"rtol" : 1e-12 },
8488 )
@@ -133,6 +137,100 @@ def test_DLQE(method):
133137 L , P , poles = dlqe (A , G , C , QN , RN , method = method )
134138 check_DLQE (L , P , poles , G , QN , RN )
135139
140+
141+ @pytest .mark .parametrize ("method" , [
142+ None , pytest .param ('slycot' , marks = pytest .mark .slycot ), 'scipy' ])
143+ @pytest .mark .parametrize ("a" , [0. , 0.7 , 1.2 ])
144+ @pytest .mark .parametrize ("return_filter_form" , [False , True ])
145+ def test_dlqe_scalar_gain (method , a , return_filter_form ):
146+ # The scalar DARE is P**2 + (r * (1 - a**2) - q) * P - q * r = 0.
147+ g , qn , r = 0.3 , 2. , 0.7
148+ q = g ** 2 * qn
149+ b = r * (1 - a ** 2 ) - q
150+ p = (- b + np .sqrt (b ** 2 + 4 * q * r )) / 2
151+ m = p / (p + r )
152+
153+ L , P , E = dlqe (a , g , 1 , qn , r , method = method ,
154+ return_filter_form = return_filter_form )
155+ assert L .shape == P .shape == (1 , 1 )
156+ assert E .shape == (1 ,)
157+ np .testing .assert_allclose (P , [[p ]], rtol = 1e-10 )
158+ np .testing .assert_allclose (L , [[m if return_filter_form else a * m ]],
159+ rtol = 1e-10 , atol = 1e-14 )
160+ np .testing .assert_allclose (E , [a * (1 - m )], rtol = 1e-10 , atol = 1e-14 )
161+
162+
163+ @pytest .mark .parametrize ("method" , [
164+ None , pytest .param ('slycot' , marks = pytest .mark .slycot ), 'scipy' ])
165+ @pytest .mark .parametrize ("A" , [
166+ [[1.1 , 0.2 , 0 ], [0 , 0.7 , 0.1 ], [0 , 0 , 0.3 ]],
167+ [[1.1 , 0.2 , 0 ], [0 , 0.7 , 0.1 ], [0 , 0 , 0 ]],
168+ ])
169+ def test_dlqe_filter_form_covariance (A , method ):
170+ A = np .array (A )
171+ G = np .array ([[1 , 0.25 ], [0.5 , 1 ], [1 , - 0.5 ]])
172+ C = np .array ([[1 , 0 , 0.25 ], [0.5 , 1 , 0 ]])
173+ QN = np .array ([[0.5 , 0.125 ], [0.125 , 0.75 ]])
174+ RN = np .array ([[0.75 , 0.25 ], [0.25 , 1.5 ]])
175+
176+ # Independently converge the measurement update and prediction, using
177+ # the Joseph form to compute the posterior covariance.
178+ prior = np .eye (3 )
179+ for _ in range (200 ):
180+ M = np .linalg .solve (C @ prior @ C .T + RN , C @ prior ).T
181+ correction = np .eye (3 ) - M @ C
182+ posterior = correction @ prior @ correction .T + M @ RN @ M .T
183+ next_prior = A @ posterior @ A .T + G @ QN @ G .T
184+ if np .linalg .norm (next_prior - prior ) < 1e-13 :
185+ break
186+ prior = next_prior
187+ else :
188+ pytest .fail ("Kalman covariance iteration did not converge" )
189+
190+ L , P , E = dlqe (A , G , C , QN , RN , method = method )
191+ Lpred , Ppred , Epred = dlqe (A , G , C , QN , RN , method = method ,
192+ return_filter_form = False )
193+ Lfilter , Pfilter , Efilter = dlqe (A , G , C , QN , RN , method = method ,
194+ return_filter_form = True )
195+ np .testing .assert_allclose (P , prior , rtol = 1e-10 , atol = 1e-12 )
196+ np .testing .assert_allclose (Lfilter , M , rtol = 1e-10 , atol = 1e-12 )
197+ np .testing .assert_allclose (L , A @ M , rtol = 1e-10 , atol = 1e-12 )
198+ np .testing .assert_allclose (Lpred , L )
199+ np .testing .assert_allclose (Ppred , P )
200+ np .testing .assert_allclose (Pfilter , P )
201+ np .testing .assert_allclose (Epred , E )
202+ np .testing .assert_allclose (Efilter , E )
203+ np .testing .assert_allclose (
204+ np .sort_complex (E ), np .sort_complex (np .linalg .eigvals (A @ correction )),
205+ rtol = 1e-10 , atol = 1e-12 )
206+ assert np .max (np .abs (E )) < 1
207+
208+ # The two-step filter and the existing predictor give the same next
209+ # estimate, even when A is singular and cannot recover M from A @ M.
210+ xhat , innovation = np .array ([0.1 , - 0.2 , 0.3 ]), np .array ([0.4 , - 0.5 ])
211+ np .testing .assert_allclose (
212+ A @ (xhat + Lfilter @ innovation ), A @ xhat + L @ innovation )
213+ np .testing .assert_allclose (
214+ P , A @ posterior @ A .T + G @ QN @ G .T , rtol = 1e-10 , atol = 1e-12 )
215+
216+
217+ @pytest .mark .parametrize ("return_filter_form" , [False , True ])
218+ @pytest .mark .parametrize ("dt" , [True , 0.1 ])
219+ def test_dlqe_filter_form_call_format (return_filter_form , dt ):
220+ sys = ct .ss ([[0.7 , 0.2 ], [0 , 0 ]], [[1 ], [0.3 ]], [[1 , 0.4 ]], 0 , dt = dt )
221+ expected = dlqe (sys .A , sys .B , sys .C , 0.5 , 0.8 ,
222+ return_filter_form = return_filter_form )
223+ for cdlqe in (dlqe , lqe ):
224+ result = cdlqe (sys , 0.5 , 0.8 , return_filter_form = return_filter_form )
225+ for actual , reference in zip (result , expected ):
226+ np .testing .assert_allclose (actual , reference )
227+ with pytest .raises (ct .ControlNotImplemented , match = "cross-covariance" ):
228+ cdlqe (sys , 0.5 , 0.8 , 0 , return_filter_form = return_filter_form )
229+ with pytest .raises (TypeError , match = "unrecognized keyword" ):
230+ cdlqe (sys , 0.5 , 0.8 , return_filter_form = return_filter_form ,
231+ unknown = True )
232+
233+
136234def test_lqe_discrete ():
137235 """Test overloading of lqe operator for discrete-time systems"""
138236 csys = ct .rss (2 , 1 , 1 )
0 commit comments