Skip to content

Commit 91a0455

Browse files
committed
add method='scipy' to lqr()
1 parent 4f681ab commit 91a0455

File tree

2 files changed

+61
-23
lines changed

2 files changed

+61
-23
lines changed

control/statefbk.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ def lqr(*args, **keywords):
475475
State and input weight matrices
476476
N : 2D array, optional
477477
Cross weight matrix
478+
method : str, optional
479+
Set the method used for computing the result. Current methods are
480+
'slycot' and 'scipy'. If set to None (default), try 'slycot' first
481+
and then 'scipy'.
478482
479483
Returns
480484
-------
@@ -498,14 +502,23 @@ def lqr(*args, **keywords):
498502
--------
499503
>>> K, S, E = lqr(sys, Q, R, [N])
500504
>>> K, S, E = lqr(A, B, Q, R, [N])
505+
501506
"""
502507

503-
# Make sure that SLICOT is installed
504-
try:
505-
from slycot import sb02md
506-
from slycot import sb02mt
507-
except ImportError:
508-
raise ControlSlycot("can't find slycot module 'sb02md' or 'sb02nt'")
508+
# Figure out what method to use
509+
method = keywords.get('method', None)
510+
if method == 'slycot' or method is None:
511+
# Make sure that SLICOT is installed
512+
try:
513+
from slycot import sb02md
514+
from slycot import sb02mt
515+
method = 'slycot'
516+
except ImportError:
517+
if method == 'slycot':
518+
raise ControlSlycot(
519+
"can't find slycot module 'sb02md' or 'sb02nt'")
520+
else:
521+
method = 'scipy'
509522

510523
#
511524
# Process the arguments and figure out what inputs we received
@@ -546,18 +559,28 @@ def lqr(*args, **keywords):
546559
N.shape[0] != nstates or N.shape[1] != ninputs):
547560
raise ControlDimension("incorrect weighting matrix dimensions")
548561

549-
# Compute the G matrix required by SB02MD
550-
A_b, B_b, Q_b, R_b, L_b, ipiv, oufact, G = \
551-
sb02mt(nstates, ninputs, B, R, A, Q, N, jobl='N')
562+
if method == 'slycot':
563+
# Compute the G matrix required by SB02MD
564+
A_b, B_b, Q_b, R_b, L_b, ipiv, oufact, G = \
565+
sb02mt(nstates, ninputs, B, R, A, Q, N, jobl='N')
552566

553-
# Call the SLICOT function
554-
X, rcond, w, S, U, A_inv = sb02md(nstates, A_b, G, Q_b, 'C')
567+
# Call the SLICOT function
568+
X, rcond, w, S, U, A_inv = sb02md(nstates, A_b, G, Q_b, 'C')
555569

556-
# Now compute the return value
557-
# We assume that R is positive definite and, hence, invertible
558-
K = np.linalg.solve(R, B.T @ X + N.T)
559-
S = X
560-
E = w[0:nstates]
570+
# Now compute the return value
571+
# We assume that R is positive definite and, hence, invertible
572+
K = np.linalg.solve(R, np.dot(B.T, X) + N.T)
573+
S = X
574+
E = w[0:nstates]
575+
576+
elif method == 'scipy':
577+
import scipy as sp
578+
S = sp.linalg.solve_continuous_are(A, B, Q, R, s=N)
579+
K = np.linalg.solve(R, B.T @ S + N.T)
580+
E, _ = np.linalg.eig(A - B @ K)
581+
582+
else:
583+
raise ValueError("unknown method: %s" % method)
561584

562585
return _ssmatrix(K), _ssmatrix(S), E
563586

control/tests/statefbk_test.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import control as ct
1010
from control import lqe, pole, rss, ss, tf
11-
from control.exception import ControlDimension
11+
from control.exception import ControlDimension, ControlSlycot, slycot_check
1212
from control.mateqn import care, dare
1313
from control.statefbk import ctrb, obsv, place, place_varga, lqr, gram, acker
1414
from control.tests.conftest import (slycotonly, check_deprecated_matrix,
@@ -306,19 +306,34 @@ def check_LQR(self, K, S, poles, Q, R):
306306
np.testing.assert_array_almost_equal(poles, poles_expected)
307307

308308

309-
@slycotonly
310-
def test_LQR_integrator(self, matarrayin, matarrayout):
309+
@pytest.mark.parametrize("method", [None, 'slycot', 'scipy'])
310+
def test_LQR_integrator(self, matarrayin, matarrayout, method):
311+
if method == 'slycot' and not slycot_check():
312+
return
311313
A, B, Q, R = (matarrayin([[X]]) for X in [0., 1., 10., 2.])
312-
K, S, poles = lqr(A, B, Q, R)
314+
K, S, poles = lqr(A, B, Q, R, method=method)
313315
self.check_LQR(K, S, poles, Q, R)
314316

315-
@slycotonly
316-
def test_LQR_3args(self, matarrayin, matarrayout):
317+
@pytest.mark.parametrize("method", [None, 'slycot', 'scipy'])
318+
def test_LQR_3args(self, matarrayin, matarrayout, method):
319+
if method == 'slycot' and not slycot_check():
320+
return
317321
sys = ss(0., 1., 1., 0.)
318322
Q, R = (matarrayin([[X]]) for X in [10., 2.])
319-
K, S, poles = lqr(sys, Q, R)
323+
K, S, poles = lqr(sys, Q, R, method=method)
320324
self.check_LQR(K, S, poles, Q, R)
321325

326+
def test_lqr_badmethod(self):
327+
A, B, Q, R = 0, 1, 10, 2
328+
with pytest.raises(ValueError, match="unknown"):
329+
K, S, poles = lqr(A, B, Q, R, method='nosuchmethod')
330+
331+
def test_lqr_slycot_not_installed(self):
332+
A, B, Q, R = 0, 1, 10, 2
333+
if not slycot_check():
334+
with pytest.raises(ControlSlycot, match="can't find slycot"):
335+
K, S, poles = lqr(A, B, Q, R, method='slycot')
336+
322337
@slycotonly
323338
@pytest.mark.xfail(reason="warning not implemented")
324339
def testLQR_warning(self):

0 commit comments

Comments
 (0)