Skip to content

Commit 700ff80

Browse files
committed
continuous time system support for create_estimator_iosystem
1 parent 508dc7f commit 700ff80

File tree

2 files changed

+113
-27
lines changed

2 files changed

+113
-27
lines changed

control/stochsys.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import scipy as sp
2121
from math import sqrt
2222

23-
from .iosys import InputOutputSystem, NonlinearIOSystem
23+
from .iosys import InputOutputSystem, LinearIOSystem, NonlinearIOSystem
2424
from .lti import LTI
2525
from .namedio import isctime, isdtime
2626
from .mateqn import care, dare, _check_shape
@@ -384,8 +384,8 @@ def create_estimator_iosystem(
384384
"""
385385

386386
# Make sure that we were passed an I/O system as an input
387-
if not isinstance(sys, InputOutputSystem):
388-
raise ControlArgument("Input system must be I/O system")
387+
if not isinstance(sys, LinearIOSystem):
388+
raise ControlArgument("Input system must be a linear I/O system")
389389

390390
# Extract the matrices that we need for easy reference
391391
A, B = sys.A, sys.B
@@ -409,7 +409,7 @@ def create_estimator_iosystem(
409409
# Initialize the covariance matrix
410410
if P0 is None:
411411
# Initalize P0 to the steady state value
412-
_, P0, _ = lqe(A, G, C, QN, RN)
412+
L0, P0, _ = lqe(A, G, C, QN, RN)
413413

414414
# Figure out the labels to use
415415
if isinstance(state_labels, str):
@@ -432,24 +432,54 @@ def create_estimator_iosystem(
432432
sensor_labels = [sensor_labels.format(i=i) for i in range(C.shape[0])]
433433

434434
if isctime(sys):
435-
raise NotImplementedError("continuous time not yet implemented")
436-
437-
else:
438435
# Create an I/O system for the state feedback gains
439436
# Note: reshape vectors into column vectors for legacy np.matrix
437+
438+
R_inv = np.linalg.inv(RN)
439+
Reps_inv = C.T @ R_inv @ C
440+
441+
def _estim_update(t, x, u, params):
442+
# See if we are estimating or predicting
443+
correct = params.get('correct', True)
444+
445+
# Get the state of the estimator
446+
xhat = x[0:sys.nstates].reshape(-1, 1)
447+
P = x[sys.nstates:].reshape(sys.nstates, sys.nstates)
448+
449+
# Extract the inputs to the estimator
450+
y = u[0:C.shape[0]].reshape(-1, 1)
451+
u = u[C.shape[0]:].reshape(-1, 1)
452+
453+
# Compute the optimal gain
454+
L = P @ C.T @ R_inv
455+
456+
# Update the state estimate
457+
dxhat = A @ xhat + B @ u # prediction
458+
if correct:
459+
dxhat -= L @ (C @ xhat - y) # correction
460+
461+
# Update the covariance
462+
dP = A @ P + P @ A.T + G @ QN @ G.T
463+
if correct:
464+
dP -= P @ Reps_inv @ P
465+
466+
# Return the update
467+
return np.hstack([dxhat.reshape(-1), dP.reshape(-1)])
468+
469+
else:
440470
def _estim_update(t, x, u, params):
441471
# See if we are estimating or predicting
442472
correct = params.get('correct', True)
443473

444-
# Get the state of the estimator
474+
# Get the state of the estimator
445475
xhat = x[0:sys.nstates].reshape(-1, 1)
446476
P = x[sys.nstates:].reshape(sys.nstates, sys.nstates)
447477

448478
# Extract the inputs to the estimator
449479
y = u[0:C.shape[0]].reshape(-1, 1)
450480
u = u[C.shape[0]:].reshape(-1, 1)
451481

452-
# Compute the optimal again
482+
# Compute the optimal gain
453483
Reps_inv = np.linalg.inv(RN + C @ P @ C.T)
454484
L = A @ P @ C.T @ Reps_inv
455485

@@ -466,8 +496,8 @@ def _estim_update(t, x, u, params):
466496
# Return the update
467497
return np.hstack([dxhat.reshape(-1), dP.reshape(-1)])
468498

469-
def _estim_output(t, x, u, params):
470-
return x[0:sys.nstates]
499+
def _estim_output(t, x, u, params):
500+
return x[0:sys.nstates]
471501

472502
# Define the estimator system
473503
return NonlinearIOSystem(

control/tests/stochsys_test.py

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import control as ct
99
from 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
1213
def 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+
122123
def 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,75 @@ 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+
203+
# Make sure that we converge to the optimal estimate
204+
estim_resp = ct.input_output_response(
205+
estim, timepts, [sys_resp.outputs, U], [0*X0, P0])
206+
np.testing.assert_allclose(
207+
estim_resp.states[0:sys.nstates, -1], sys_resp.states[:, -1],
208+
atol=1e-6, rtol=1e-3)
209+
np.testing.assert_allclose(
210+
estim_resp.states[sys.nstates:, -1], Pf.reshape(-1),
211+
atol=1e-6, rtol=1e-3)
212+
213+
# Make sure that optimal estimate is an eq pt
214+
ss_resp = ct.input_output_response(
215+
estim, timepts, [sys_resp.outputs, U], [X0, Pf])
216+
np.testing.assert_allclose(
217+
ss_resp.states[sys.nstates:],
218+
np.outer(Pf.reshape(-1), np.ones_like(timepts)),
219+
atol=1e-4, rtol=1e-2)
220+
np.testing.assert_allclose(
221+
ss_resp.states[0:sys.nstates], sys_resp.states,
222+
atol=1e-4, rtol=1e-2)
223+
224+
165225
def test_estimator_errors():
166226
sys = ct.drss(4, 2, 2, strictly_proper=True)
167227
P0 = np.eye(sys.nstates)
168228
QN = np.eye(sys.ninputs)
169229
RN = np.eye(sys.noutputs)
170230

171-
with pytest.raises(ct.ControlArgument, match="Input system must be I/O"):
231+
with pytest.raises(ct.ControlArgument, match=".* system must be a linear"):
172232
sys_tf = ct.tf([1], [1, 1], dt=True)
173233
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-
234+
179235
with pytest.raises(ValueError, match="output must be full state"):
180236
C = np.eye(2, 4)
181237
estim = ct.create_estimator_iosystem(sys, QN, RN, C=C)
@@ -246,7 +302,7 @@ def test_correlation():
246302
# Try passing a second argument
247303
tau, Rneg = ct.correlation(T, V, -V)
248304
np.testing.assert_equal(Rtau, -Rneg)
249-
305+
250306
# Test error conditions
251307
with pytest.raises(ValueError, match="Time vector T must be 1D"):
252308
tau, Rtau = ct.correlation(V, V)

0 commit comments

Comments
 (0)