Skip to content

Commit fdd399f

Browse files
committed
require StateSpace for lqr/lqe first arg + process uniformly
1 parent f1f5375 commit fdd399f

2 files changed

Lines changed: 82 additions & 48 deletions

File tree

control/statefbk.py

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444

4545
from . import statesp
4646
from .mateqn import care, dare, _check_shape
47-
from .statesp import _ssmatrix, _convert_to_statespace
47+
from .statesp import StateSpace, _ssmatrix, _convert_to_statespace
4848
from .lti import LTI, isdtime, isctime
4949
from .exception import ControlSlycot, ControlArgument, ControlDimension, \
5050
ControlNotImplemented
@@ -366,19 +366,18 @@ def lqe(*args, **keywords):
366366
# Call dlqe
367367
return dlqe(*args, **keywords)
368368

369-
try:
370-
sys = args[0] # Treat the first argument as a system
371-
if isinstance(sys, LTI):
372-
# Convert LTI system to state space
373-
sys = _convert_to_statespace(sys)
374-
375-
# Extract A, G (assume disturbances come through input), and C
376-
A = np.array(sys.A, ndmin=2, dtype=float)
377-
G = np.array(sys.B, ndmin=2, dtype=float)
378-
C = np.array(sys.C, ndmin=2, dtype=float)
369+
# If we were passed a state space system, use that to get system matrices
370+
if isinstance(args[0], StateSpace):
371+
A = np.array(args[0].A, ndmin=2, dtype=float)
372+
G = np.array(args[0].B, ndmin=2, dtype=float)
373+
C = np.array(args[0].C, ndmin=2, dtype=float)
379374
index = 1
380375

381-
except AttributeError:
376+
elif isinstance(args[0], LTI):
377+
# Don't allow other types of LTI systems
378+
raise ControlArgument("LTI system must be in state space form")
379+
380+
else:
382381
# Arguments should be A and B matrices
383382
A = np.array(args[0], ndmin=2, dtype=float)
384383
G = np.array(args[1], ndmin=2, dtype=float)
@@ -490,14 +489,18 @@ def dlqe(*args, **keywords):
490489
if isinstance(args[0], LTI) and isctime(args[0], strict=True):
491490
raise ControlArgument("dlqr() called with a continuous time system")
492491

493-
try:
494-
# If this works, we were (probably) passed a system as the
495-
# first argument; extract A and B
492+
# If we were passed a state space system, use that to get system matrices
493+
if isinstance(args[0], StateSpace):
496494
A = np.array(args[0].A, ndmin=2, dtype=float)
497495
G = np.array(args[0].B, ndmin=2, dtype=float)
498496
C = np.array(args[0].C, ndmin=2, dtype=float)
499497
index = 1
500-
except AttributeError:
498+
499+
elif isinstance(args[0], LTI):
500+
# Don't allow other types of LTI systems
501+
raise ControlArgument("LTI system must be in state space form")
502+
503+
else:
501504
# Arguments should be A and B matrices
502505
A = np.array(args[0], ndmin=2, dtype=float)
503506
G = np.array(args[1], ndmin=2, dtype=float)
@@ -517,6 +520,9 @@ def dlqe(*args, **keywords):
517520
NN = np.array(args[index+2], ndmin=2, dtype=float)
518521
raise ControlNotImplemented("cross-covariance not yet implememented")
519522

523+
# Check dimensions of G (needed before calling care())
524+
_check_shape("QN", QN, G.shape[1], G.shape[1])
525+
520526
# Compute the result (dimension and symmetry checking done in dare())
521527
P, E, LT = dare(A.T, C.T, G @ QN @ G.T, RN, method=method,
522528
B_s="C", Q_s="QN", R_s="RN", S_s="NN")
@@ -650,13 +656,17 @@ def lqr(*args, **keywords):
650656
# Call dlqr
651657
return dlqr(*args, **keywords)
652658

653-
try:
654-
# If this works, we were (probably) passed a system as the
655-
# first argument; extract A and B
659+
# If we were passed a state space system, use that to get system matrices
660+
if isinstance(args[0], StateSpace):
656661
A = np.array(args[0].A, ndmin=2, dtype=float)
657662
B = np.array(args[0].B, ndmin=2, dtype=float)
658663
index = 1
659-
except AttributeError:
664+
665+
elif isinstance(args[0], LTI):
666+
# Don't allow other types of LTI systems
667+
raise ControlArgument("LTI system must be in state space form")
668+
669+
else:
660670
# Arguments should be A and B matrices
661671
A = np.array(args[0], ndmin=2, dtype=float)
662672
B = np.array(args[1], ndmin=2, dtype=float)
@@ -746,13 +756,17 @@ def dlqr(*args, **keywords):
746756
if isinstance(args[0], LTI) and isctime(args[0], strict=True):
747757
raise ControlArgument("dsys must be discrete time (dt != 0)")
748758

749-
try:
750-
# If this works, we were (probably) passed a system as the
751-
# first argument; extract A and B
759+
# If we were passed a state space system, use that to get system matrices
760+
if isinstance(args[0], StateSpace):
752761
A = np.array(args[0].A, ndmin=2, dtype=float)
753762
B = np.array(args[0].B, ndmin=2, dtype=float)
754763
index = 1
755-
except AttributeError:
764+
765+
elif isinstance(args[0], LTI):
766+
# Don't allow other types of LTI systems
767+
raise ControlArgument("LTI system must be in state space form")
768+
769+
else:
756770
# Arguments should be A and B matrices
757771
A = np.array(args[0], ndmin=2, dtype=float)
758772
B = np.array(args[1], ndmin=2, dtype=float)

control/tests/statefbk_test.py

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -345,16 +345,18 @@ def test_DLQR_4args(self, matarrayin, matarrayout):
345345
K, S, poles = dlqr(A, B, Q, R)
346346
self.check_DLQR(K, S, poles, Q, R)
347347

348-
def test_lqr_badmethod(self):
348+
@pytest.mark.parametrize("cdlqr", [lqr, dlqr])
349+
def test_lqr_badmethod(self, cdlqr):
349350
A, B, Q, R = 0, 1, 10, 2
350351
with pytest.raises(ControlArgument, match="Unknown method"):
351-
K, S, poles = lqr(A, B, Q, R, method='nosuchmethod')
352+
K, S, poles = cdlqr(A, B, Q, R, method='nosuchmethod')
352353

353-
def test_lqr_slycot_not_installed(self):
354+
@pytest.mark.parametrize("cdlqr", [lqr, dlqr])
355+
def test_lqr_slycot_not_installed(self, cdlqr):
354356
A, B, Q, R = 0, 1, 10, 2
355357
if not slycot_check():
356358
with pytest.raises(ControlSlycot, match="Can't find slycot"):
357-
K, S, poles = lqr(A, B, Q, R, method='slycot')
359+
K, S, poles = cdlqr(A, B, Q, R, method='slycot')
358360

359361
@pytest.mark.xfail(reason="warning not implemented")
360362
def testLQR_warning(self):
@@ -375,37 +377,49 @@ def testLQR_warning(self):
375377
with pytest.warns(UserWarning):
376378
(K, S, E) = lqr(A, B, Q, R, N)
377379

378-
def test_lqr_call_format(self):
380+
@pytest.mark.parametrize("cdlqr", [lqr, dlqr])
381+
def test_lqr_call_format(self, cdlqr):
379382
# Create a random state space system for testing
380383
sys = rss(2, 3, 2)
384+
sys.dt = None # treat as either continuous or discrete time
381385

382386
# Weighting matrices
383387
Q = np.eye(sys.nstates)
384388
R = np.eye(sys.ninputs)
385389
N = np.zeros((sys.nstates, sys.ninputs))
386390

387391
# Standard calling format
388-
Kref, Sref, Eref = lqr(sys.A, sys.B, Q, R)
392+
Kref, Sref, Eref = cdlqr(sys.A, sys.B, Q, R)
389393

390394
# Call with system instead of matricees
391-
K, S, E = lqr(sys, Q, R)
395+
K, S, E = cdlqr(sys, Q, R)
392396
np.testing.assert_array_almost_equal(Kref, K)
393397
np.testing.assert_array_almost_equal(Sref, S)
394398
np.testing.assert_array_almost_equal(Eref, E)
395399

396400
# Pass a cross-weighting matrix
397-
K, S, E = lqr(sys, Q, R, N)
401+
K, S, E = cdlqr(sys, Q, R, N)
398402
np.testing.assert_array_almost_equal(Kref, K)
399403
np.testing.assert_array_almost_equal(Sref, S)
400404
np.testing.assert_array_almost_equal(Eref, E)
401405

402406
# Inconsistent system dimensions
403407
with pytest.raises(ct.ControlDimension, match="Incompatible dimen"):
404-
K, S, E = lqr(sys.A, sys.C, Q, R)
408+
K, S, E = cdlqr(sys.A, sys.C, Q, R)
405409

406-
# incorrect covariance matrix dimensions
410+
# Incorrect covariance matrix dimensions
407411
with pytest.raises(ct.ControlDimension, match="Q must be a square"):
408-
K, S, E = lqr(sys.A, sys.B, sys.C, R, Q)
412+
K, S, E = cdlqr(sys.A, sys.B, sys.C, R, Q)
413+
414+
# Too few input arguments
415+
with pytest.raises(ct.ControlArgument, match="not enough input"):
416+
K, S, E = cdlqr(sys.A, sys.B)
417+
418+
# First argument is the wrong type (use SISO for non-slycot tests)
419+
sys_tf = tf(rss(3, 1, 1))
420+
sys_tf.dt = None # treat as either continuous or discrete time
421+
with pytest.raises(ct.ControlArgument, match="LTI system must be"):
422+
K, S, E = cdlqr(sys_tf, Q, R)
409423

410424
@pytest.mark.xfail(reason="warning not implemented")
411425
def testDLQR_warning(self):
@@ -443,41 +457,47 @@ def test_LQE(self, matarrayin, method):
443457
L, P, poles = lqe(A, G, C, QN, RN, method=method)
444458
self.check_LQE(L, P, poles, G, QN, RN)
445459

446-
def test_lqe_call_format(self):
460+
@pytest.mark.parametrize("cdlqe", [lqe, dlqe])
461+
def test_lqe_call_format(self, cdlqe):
447462
# Create a random state space system for testing
448463
sys = rss(4, 3, 2)
464+
sys.dt = None # treat as either continuous or discrete time
449465

450466
# Covariance matrices
451467
Q = np.eye(sys.ninputs)
452468
R = np.eye(sys.noutputs)
453469
N = np.zeros((sys.ninputs, sys.noutputs))
454470

455471
# Standard calling format
456-
Lref, Pref, Eref = lqe(sys.A, sys.B, sys.C, Q, R)
472+
Lref, Pref, Eref = cdlqe(sys.A, sys.B, sys.C, Q, R)
457473

458474
# Call with system instead of matricees
459-
L, P, E = lqe(sys, Q, R)
475+
L, P, E = cdlqe(sys, Q, R)
460476
np.testing.assert_array_almost_equal(Lref, L)
461477
np.testing.assert_array_almost_equal(Pref, P)
462478
np.testing.assert_array_almost_equal(Eref, E)
463479

464-
# Compare state space and transfer function (SISO only)
465-
sys_siso = rss(4, 1, 1)
466-
L_ss, P_ss, E_ss = lqe(sys_siso, np.eye(1), np.eye(1))
467-
L_tf, P_tf, E_tf = lqe(tf(sys_siso), np.eye(1), np.eye(1))
468-
np.testing.assert_array_almost_equal(np.sort(E_ss), np.sort(E_tf))
469-
470480
# Make sure we get an error if we specify N
471481
with pytest.raises(ct.ControlNotImplemented):
472-
L, P, E = lqe(sys, Q, R, N)
482+
L, P, E = cdlqe(sys, Q, R, N)
473483

474484
# Inconsistent system dimensions
475485
with pytest.raises(ct.ControlDimension, match="Incompatible"):
476-
L, P, E = lqe(sys.A, sys.C, sys.B, Q, R)
486+
L, P, E = cdlqe(sys.A, sys.C, sys.B, Q, R)
477487

478-
# incorrect covariance matrix dimensions
488+
# Incorrect covariance matrix dimensions
479489
with pytest.raises(ct.ControlDimension, match="Incompatible"):
480-
L, P, E = lqe(sys.A, sys.B, sys.C, R, Q)
490+
L, P, E = cdlqe(sys.A, sys.B, sys.C, R, Q)
491+
492+
# Too few input arguments
493+
with pytest.raises(ct.ControlArgument, match="not enough input"):
494+
L, P, E = cdlqe(sys.A, sys.C)
495+
496+
# First argument is the wrong type (use SISO for non-slycot tests)
497+
sys_tf = tf(rss(3, 1, 1))
498+
sys_tf.dt = None # treat as either continuous or discrete time
499+
with pytest.raises(ct.ControlArgument, match="LTI system must be"):
500+
L, P, E = cdlqe(sys_tf, Q, R)
481501

482502
def check_DLQE(self, L, P, poles, G, QN, RN):
483503
P_expected = asmatarrayout(G.dot(QN).dot(G))

0 commit comments

Comments
 (0)