77
88import control as ct
99from control import lqe , dlqe , rss , drss , tf , ss , ControlArgument , slycot_check
10+ from math import log , pi
1011
1112# Utility function to check LQE answer
1213def check_LQE (L , P , poles , G , QN , RN ):
@@ -48,7 +49,7 @@ def test_lqe_call_format(cdlqe):
4849
4950 # Standard calling format
5051 Lref , Pref , Eref = cdlqe (sys .A , sys .B , sys .C , Q , R )
51-
52+
5253 # Call with system instead of matricees
5354 L , P , E = cdlqe (sys , Q , R )
5455 np .testing .assert_almost_equal (Lref , L )
@@ -58,15 +59,15 @@ def test_lqe_call_format(cdlqe):
5859 # Make sure we get an error if we specify N
5960 with pytest .raises (ct .ControlNotImplemented ):
6061 L , P , E = cdlqe (sys , Q , R , N )
61-
62+
6263 # Inconsistent system dimensions
6364 with pytest .raises (ct .ControlDimension , match = "Incompatible" ):
6465 L , P , E = cdlqe (sys .A , sys .C , sys .B , Q , R )
65-
66+
6667 # Incorrect covariance matrix dimensions
6768 with pytest .raises (ct .ControlDimension , match = "Incompatible" ):
6869 L , P , E = cdlqe (sys .A , sys .B , sys .C , R , Q )
69-
70+
7071 # Too few input arguments
7172 with pytest .raises (ct .ControlArgument , match = "not enough input" ):
7273 L , P , E = cdlqe (sys .A , sys .C )
@@ -99,26 +100,26 @@ def test_lqe_discrete():
99100 np .testing .assert_almost_equal (K_csys , K_expl )
100101 np .testing .assert_almost_equal (S_csys , S_expl )
101102 np .testing .assert_almost_equal (E_csys , E_expl )
102-
103+
103104 # Calling lqe() with a discrete time system should call dlqe()
104105 K_lqe , S_lqe , E_lqe = ct .lqe (dsys , Q , R )
105106 K_dlqe , S_dlqe , E_dlqe = ct .dlqe (dsys , Q , R )
106107 np .testing .assert_almost_equal (K_lqe , K_dlqe )
107108 np .testing .assert_almost_equal (S_lqe , S_dlqe )
108109 np .testing .assert_almost_equal (E_lqe , E_dlqe )
109-
110+
110111 # Calling lqe() with no timebase should call lqe()
111112 asys = ct .ss (csys .A , csys .B , csys .C , csys .D , dt = None )
112113 K_asys , S_asys , E_asys = ct .lqe (asys , Q , R )
113114 K_expl , S_expl , E_expl = ct .lqe (csys .A , csys .B , csys .C , Q , R )
114115 np .testing .assert_almost_equal (K_asys , K_expl )
115116 np .testing .assert_almost_equal (S_asys , S_expl )
116117 np .testing .assert_almost_equal (E_asys , E_expl )
117-
118+
118119 # Calling dlqe() with a continuous time system should raise an error
119120 with pytest .raises (ControlArgument , match = "called with a continuous" ):
120121 K , S , E = ct .dlqe (csys , Q , R )
121-
122+
122123def test_estimator_iosys ():
123124 sys = ct .drss (4 , 2 , 2 , strictly_proper = True )
124125
@@ -129,7 +130,7 @@ def test_estimator_iosys():
129130 QN = np .eye (sys .ninputs )
130131 RN = np .eye (sys .noutputs )
131132 estim = ct .create_estimator_iosystem (sys , QN , RN , P0 )
132-
133+
133134 ctrl , clsys = ct .create_statefbk_iosystem (sys , K , estimator = estim )
134135
135136 # Extract the elements of the estimator
@@ -162,20 +163,76 @@ def test_estimator_iosys():
162163 np .testing .assert_almost_equal (cls .D , D_clchk )
163164
164165
166+ @pytest .mark .parametrize ("sys_args" , [
167+ ([[- 1 ]], [[1 ]], [[1 ]], 0 ), # scalar system
168+ ([[- 1 , 0.1 ], [0 , - 2 ]], [[0 ], [1 ]], [[1 , 0 ]], 0 ), # SISO, 2 state
169+ ([[- 1 , 0.1 ], [0 , - 2 ]], [[1 , 0 ], [0 , 1 ]], [[1 , 0 ]], 0 ), # 2i, 1o, 2s
170+ ([[- 1 , 0.1 , 0.1 ], [0 , - 2 , 0 ], [0.1 , 0 , - 3 ]], # 2i, 2o, 3s
171+ [[1 , 0 ], [0 , 0.1 ], [0 , 1 ]],
172+ [[1 , 0 , 0.1 ], [0 , 1 , 0.1 ]], 0 ),
173+ ])
174+ def test_estimator_iosys_ctime (sys_args ):
175+ # Define the system we want to test
176+ sys = ct .ss (* sys_args )
177+ T = 10 * log (1e-2 ) / np .max (sys .poles ().real )
178+ assert T > 0
179+
180+ # Create nonlinear version of the system to match integration methods
181+ nl_sys = ct .NonlinearIOSystem (
182+ lambda t , x , u , params : sys .A @ x + sys .B @ u ,
183+ lambda t , x , u , params : sys .C @ x + sys .D @ u ,
184+ inputs = sys .ninputs , outputs = sys .noutputs , states = sys .nstates )
185+
186+ # Define an initial condition, inputs (small, to avoid integration errors)
187+ timepts = np .linspace (0 , T , 500 )
188+ U = 2e-2 * np .array ([np .sin (timepts + i * pi / 3 ) for i in range (sys .ninputs )])
189+ X0 = np .ones (sys .nstates )
190+
191+ # Set up the parameters for the filter
192+ P0 = np .eye (sys .nstates )
193+ QN = np .eye (sys .ninputs )
194+ RN = np .eye (sys .noutputs )
195+
196+ # Construct the estimator
197+ estim = ct .create_estimator_iosystem (sys , QN , RN )
198+
199+ # Compute the system response and the optimal covariance
200+ sys_resp = ct .input_output_response (nl_sys , timepts , U , X0 )
201+ _ , Pf , _ = ct .lqe (sys , QN , RN )
202+ Pf = np .array (Pf ) # convert from matrix, if needed
203+
204+ # Make sure that we converge to the optimal estimate
205+ estim_resp = ct .input_output_response (
206+ estim , timepts , [sys_resp .outputs , U ], [0 * X0 , P0 ])
207+ np .testing .assert_allclose (
208+ estim_resp .states [0 :sys .nstates , - 1 ], sys_resp .states [:, - 1 ],
209+ atol = 1e-6 , rtol = 1e-3 )
210+ np .testing .assert_allclose (
211+ estim_resp .states [sys .nstates :, - 1 ], Pf .reshape (- 1 ),
212+ atol = 1e-6 , rtol = 1e-3 )
213+
214+ # Make sure that optimal estimate is an eq pt
215+ ss_resp = ct .input_output_response (
216+ estim , timepts , [sys_resp .outputs , U ], [X0 , Pf ])
217+ np .testing .assert_allclose (
218+ ss_resp .states [sys .nstates :],
219+ np .outer (Pf .reshape (- 1 ), np .ones_like (timepts )),
220+ atol = 1e-4 , rtol = 1e-2 )
221+ np .testing .assert_allclose (
222+ ss_resp .states [0 :sys .nstates ], sys_resp .states ,
223+ atol = 1e-4 , rtol = 1e-2 )
224+
225+
165226def test_estimator_errors ():
166227 sys = ct .drss (4 , 2 , 2 , strictly_proper = True )
167228 P0 = np .eye (sys .nstates )
168229 QN = np .eye (sys .ninputs )
169230 RN = np .eye (sys .noutputs )
170231
171- with pytest .raises (ct .ControlArgument , match = "Input system must be I/O " ):
232+ with pytest .raises (ct .ControlArgument , match = ".* system must be a linear " ):
172233 sys_tf = ct .tf ([1 ], [1 , 1 ], dt = True )
173234 estim = ct .create_estimator_iosystem (sys_tf , QN , RN )
174-
175- with pytest .raises (NotImplementedError , match = "continuous time not" ):
176- sys_ct = ct .rss (4 , 2 , 2 , strictly_proper = True )
177- estim = ct .create_estimator_iosystem (sys_ct , QN , RN )
178-
235+
179236 with pytest .raises (ValueError , match = "output must be full state" ):
180237 C = np .eye (2 , 4 )
181238 estim = ct .create_estimator_iosystem (sys , QN , RN , C = C )
@@ -246,7 +303,7 @@ def test_correlation():
246303 # Try passing a second argument
247304 tau , Rneg = ct .correlation (T , V , - V )
248305 np .testing .assert_equal (Rtau , - Rneg )
249-
306+
250307 # Test error conditions
251308 with pytest .raises (ValueError , match = "Time vector T must be 1D" ):
252309 tau , Rtau = ct .correlation (V , V )
0 commit comments