Skip to content

Commit cf3c9b2

Browse files
committed
remove redundant argument process in care/dare + fix up error strings
1 parent 20b0312 commit cf3c9b2

File tree

3 files changed

+65
-103
lines changed

3 files changed

+65
-103
lines changed

control/mateqn.py

+31-53
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,8 @@ def dlyap(A, Q, C=None, E=None, method=None):
343343
# Riccati equation solvers care and dare
344344
#
345345

346-
def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None):
346+
def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None,
347+
A_s="A", B_s="B", Q_s="Q", R_s="R", S_s="S", E_s="E"):
347348
"""X, L, G = care(A, B, Q, R=None) solves the continuous-time
348349
algebraic Riccati equation
349350
@@ -428,10 +429,10 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None):
428429
m = B.shape[1]
429430

430431
# Check to make sure input matrices are the right shape and type
431-
_check_shape("A", A, n, n, square=True)
432-
_check_shape("B", B, n, m)
433-
_check_shape("Q", Q, n, n, square=True, symmetric=True)
434-
_check_shape("R", R, m, m, square=True, symmetric=True)
432+
_check_shape(A_s, A, n, n, square=True)
433+
_check_shape(B_s, B, n, m)
434+
_check_shape(Q_s, Q, n, n, square=True, symmetric=True)
435+
_check_shape(R_s, R, m, m, square=True, symmetric=True)
435436

436437
# Solve the standard algebraic Riccati equation
437438
if S is None and E is None:
@@ -471,8 +472,8 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None):
471472
E = np.eye(A.shape[0]) if E is None else np.array(E, ndmin=2)
472473

473474
# Check to make sure input matrices are the right shape and type
474-
_check_shape("E", E, n, n, square=True)
475-
_check_shape("S", S, n, m)
475+
_check_shape(E_s, E, n, n, square=True)
476+
_check_shape(S_s, S, n, m)
476477

477478
# See if we should solve this using SciPy
478479
if method == 'scipy':
@@ -510,8 +511,9 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None):
510511
# the gain matrix G
511512
return _ssmatrix(X), L, _ssmatrix(G)
512513

513-
def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None):
514-
"""(X, L, G) = dare(A, B, Q, R) solves the discrete-time algebraic Riccati
514+
def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None,
515+
A_s="A", B_s="B", Q_s="Q", R_s="R", S_s="S", E_s="E"):
516+
"""X, L, G = dare(A, B, Q, R) solves the discrete-time algebraic Riccati
515517
equation
516518
517519
:math:`A^T X A - X - A^T X B (B^T X B + R)^{-1} B^T X A + Q = 0`
@@ -521,16 +523,17 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None):
521523
matrix G = (B^T X B + R)^-1 B^T X A and the closed loop eigenvalues L,
522524
i.e., the eigenvalues of A - B G.
523525
524-
(X, L, G) = dare(A, B, Q, R, S, E) solves the generalized discrete-time
526+
X, L, G = dare(A, B, Q, R, S, E) solves the generalized discrete-time
525527
algebraic Riccati equation
526528
527529
:math:`A^T X A - E^T X E - (A^T X B + S) (B^T X B + R)^{-1} (B^T X A + S^T) + Q = 0`
528530
529-
where A, Q and E are square matrices of the same dimension. Further, Q and
530-
R are symmetric matrices. If R is None, it is set to the identity
531-
matrix. The function returns the solution X, the gain
532-
matrix :math:`G = (B^T X B + R)^{-1} (B^T X A + S^T)` and the closed loop
533-
eigenvalues L, i.e., the eigenvalues of A - B G , E.
531+
where A, Q and E are square matrices of the same dimension. Further, Q
532+
and R are symmetric matrices. If R is None, it is set to the identity
533+
matrix. The function returns the solution X, the gain matrix :math:`G =
534+
(B^T X B + R)^{-1} (B^T X A + S^T)` and the closed loop eigenvalues L,
535+
i.e., the (generalized) eigenvalues of A - B G (with respect to E, if
536+
specified).
534537
535538
Parameters
536539
----------
@@ -576,7 +579,14 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None):
576579
m = B.shape[1]
577580

578581
# Check to make sure input matrices are the right shape and type
579-
_check_shape("A", A, n, n, square=True)
582+
_check_shape(A_s, A, n, n, square=True)
583+
_check_shape(B_s, B, n, m)
584+
_check_shape(Q_s, Q, n, n, square=True, symmetric=True)
585+
_check_shape(R_s, R, m, m, square=True, symmetric=True)
586+
if E is not None:
587+
_check_shape(E_s, E, n, n, square=True)
588+
if S is not None:
589+
_check_shape(S_s, S, n, m)
580590

581591
# Figure out how to solve the problem
582592
if method == 'scipy' and not stabilizing:
@@ -587,21 +597,11 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None):
587597
return _dare_slycot(A, B, Q, R, S, E, stabilizing)
588598

589599
else:
590-
_check_shape("B", B, n, m)
591-
_check_shape("Q", Q, n, n, square=True, symmetric=True)
592-
_check_shape("R", R, m, m, square=True, symmetric=True)
593-
if E is not None:
594-
_check_shape("E", E, n, n, square=True)
595-
if S is not None:
596-
_check_shape("S", S, n, m)
597-
598-
Rmat = _ssmatrix(R)
599-
Qmat = _ssmatrix(Q)
600-
X = sp.linalg.solve_discrete_are(A, B, Qmat, Rmat, e=E, s=S)
600+
X = sp.linalg.solve_discrete_are(A, B, Q, R, e=E, s=S)
601601
if S is None:
602-
G = solve(B.T @ X @ B + Rmat, B.T @ X @ A)
602+
G = solve(B.T @ X @ B + R, B.T @ X @ A)
603603
else:
604-
G = solve(B.T @ X @ B + Rmat, B.T @ X @ A + S.T)
604+
G = solve(B.T @ X @ B + R, B.T @ X @ A + S.T)
605605
if E is None:
606606
L = eigvals(A - B @ G)
607607
else:
@@ -611,7 +611,7 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None):
611611

612612

613613
def _dare_slycot(A, B, Q, R, S=None, E=None, stabilizing=True):
614-
# Make sure we can import required slycot routine
614+
# Make sure we can import required slycot routines
615615
try:
616616
from slycot import sb02md
617617
except ImportError:
@@ -622,18 +622,11 @@ def _dare_slycot(A, B, Q, R, S=None, E=None, stabilizing=True):
622622
except ImportError:
623623
raise ControlSlycot("Can't find slycot module 'sb02mt'")
624624

625-
# Make sure we can find the required slycot routine
626625
try:
627626
from slycot import sg02ad
628627
except ImportError:
629628
raise ControlSlycot("Can't find slycot module 'sg02ad'")
630629

631-
# Reshape input arrays
632-
A = np.array(A, ndmin=2)
633-
B = np.array(B, ndmin=2)
634-
Q = np.array(Q, ndmin=2)
635-
R = np.eye(B.shape[1]) if R is None else np.array(R, ndmin=2)
636-
637630
# Determine main dimensions
638631
n = A.shape[0]
639632
m = B.shape[1]
@@ -642,21 +635,6 @@ def _dare_slycot(A, B, Q, R, S=None, E=None, stabilizing=True):
642635
S = np.zeros((n, m)) if S is None else np.array(S, ndmin=2)
643636
E = np.eye(A.shape[0]) if E is None else np.array(E, ndmin=2)
644637

645-
# Check to make sure input matrices are the right shape and type
646-
_check_shape("A", A, n, n, square=True)
647-
_check_shape("B", B, n, m)
648-
_check_shape("Q", Q, n, n, square=True, symmetric=True)
649-
_check_shape("R", R, m, m, square=True, symmetric=True)
650-
_check_shape("E", E, n, n, square=True)
651-
_check_shape("S", S, n, m)
652-
653-
# Create back-up of arrays needed for later computations
654-
A_b = copy(A)
655-
R_b = copy(R)
656-
B_b = copy(B)
657-
E_b = copy(E)
658-
S_b = copy(S)
659-
660638
# Solve the generalized algebraic Riccati equation by calling the
661639
# Slycot function sg02ad
662640
sort = 'S' if stabilizing else 'U'
@@ -670,7 +648,7 @@ def _dare_slycot(A, B, Q, R, S=None, E=None, stabilizing=True):
670648
L = np.array([(alfar[i] + alfai[i]*1j) / beta[i] for i in range(n)])
671649

672650
# Calculate the gain matrix G
673-
G = solve(B_b.T @ X @ B_b + R_b, B_b.T @ X @ A_b + S_b.T)
651+
G = solve(B.T @ X @ B + R, B.T @ X @ A + S.T)
674652

675653
# Return the solution X, the closed-loop eigenvalues L and
676654
# the gain matrix G

control/statefbk.py

+32-48
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
import numpy as np
4444

4545
from . import statesp
46-
from .mateqn import care, dare
46+
from .mateqn import care, dare, _check_shape
4747
from .statesp import _ssmatrix, _convert_to_statespace
4848
from .lti import LTI, isdtime, isctime
4949
from .exception import ControlSlycot, ControlArgument, ControlDimension, \
@@ -260,7 +260,7 @@ def place_varga(A, B, p, dtime=False, alpha=None):
260260

261261
# contributed by Sawyer B. Fuller <minster@uw.edu>
262262
def lqe(*args, **keywords):
263-
"""lqe(A, G, C, Q, R, [, N])
263+
"""lqe(A, G, C, QN, RN, [, NN])
264264
265265
Linear quadratic estimator design (Kalman filter) for continuous-time
266266
systems. Given the system
@@ -272,26 +272,26 @@ def lqe(*args, **keywords):
272272
273273
with unbiased process noise w and measurement noise v with covariances
274274
275-
.. math:: E{ww'} = Q, E{vv'} = R, E{wv'} = N
275+
.. math:: E{ww'} = QN, E{vv'} = RN, E{wv'} = NN
276276
277277
The lqe() function computes the observer gain matrix L such that the
278278
stationary (non-time-varying) Kalman filter
279279
280-
.. math:: x_e = A x_e + B u + L(y - C x_e - D u)
280+
.. math:: x_e = A x_e + G u + L(y - C x_e - D u)
281281
282282
produces a state estimate x_e that minimizes the expected squared error
283-
using the sensor measurements y. The noise cross-correlation `N` is
283+
using the sensor measurements y. The noise cross-correlation `NN` is
284284
set to zero when omitted.
285285
286286
The function can be called with either 3, 4, 5, or 6 arguments:
287287
288-
* ``L, P, E = lqe(sys, Q, R)``
289-
* ``L, P, E = lqe(sys, Q, R, N)``
290-
* ``L, P, E = lqe(A, G, C, Q, R)``
291-
* ``L, P, E = lqe(A, B, C, Q, R, N)``
288+
* ``L, P, E = lqe(sys, QN, RN)``
289+
* ``L, P, E = lqe(sys, QN, RN, NN)``
290+
* ``L, P, E = lqe(A, G, C, QN, RN)``
291+
* ``L, P, E = lqe(A, G, C, QN, RN, NN)``
292292
293-
where `sys` is an `LTI` object, and `A`, `G`, `C`, `Q`, `R`, and `N` are
294-
2D arrays or matrices of appropriate dimension.
293+
where `sys` is an `LTI` object, and `A`, `G`, `C`, `QN`, `RN`, and `NN`
294+
are 2D arrays or matrices of appropriate dimension.
295295
296296
Parameters
297297
----------
@@ -300,9 +300,9 @@ def lqe(*args, **keywords):
300300
sys : LTI (StateSpace or TransferFunction)
301301
Linear I/O system, with the process noise input taken as the system
302302
input.
303-
Q, R : 2D array_like
303+
QN, RN : 2D array_like
304304
Process and sensor noise covariance matrices
305-
N : 2D array, optional
305+
NN : 2D array, optional
306306
Cross covariance matrix. Not currently implemented.
307307
method : str, optional
308308
Set the method used for computing the result. Current methods are
@@ -335,8 +335,8 @@ def lqe(*args, **keywords):
335335
336336
Examples
337337
--------
338-
>>> L, P, E = lqe(A, G, C, Q, R)
339-
>>> L, P, E = lqe(A, G, C, Q, R, N)
338+
>>> L, P, E = lqe(A, G, C, QN, RN)
339+
>>> L, P, E = lqe(A, G, C, Q, RN, NN)
340340
341341
See Also
342342
--------
@@ -386,31 +386,24 @@ def lqe(*args, **keywords):
386386
index = 3
387387

388388
# Get the weighting matrices (converting to matrices, if needed)
389-
Q = np.array(args[index], ndmin=2, dtype=float)
390-
R = np.array(args[index+1], ndmin=2, dtype=float)
389+
QN = np.array(args[index], ndmin=2, dtype=float)
390+
RN = np.array(args[index+1], ndmin=2, dtype=float)
391391

392392
# Get the cross-covariance matrix, if given
393393
if (len(args) > index + 2):
394-
N = np.array(args[index+2], ndmin=2, dtype=float)
394+
NN = np.array(args[index+2], ndmin=2, dtype=float)
395395
raise ControlNotImplemented("cross-covariance not implemented")
396396

397397
else:
398-
N = np.zeros((Q.shape[0], R.shape[1]))
399-
400-
# Check dimensions for consistency
401-
nstates = A.shape[0]
402-
ninputs = G.shape[1]
403-
noutputs = C.shape[0]
404-
if (A.shape[0] != nstates or A.shape[1] != nstates or
405-
G.shape[0] != nstates or C.shape[1] != nstates):
406-
raise ControlDimension("inconsistent system dimensions")
398+
# For future use (not currently used below)
399+
NN = np.zeros((QN.shape[0], RN.shape[1]))
407400

408-
elif (Q.shape[0] != ninputs or Q.shape[1] != ninputs or
409-
R.shape[0] != noutputs or R.shape[1] != noutputs or
410-
N.shape[0] != ninputs or N.shape[1] != noutputs):
411-
raise ControlDimension("incorrect covariance matrix dimensions")
401+
# Check dimensions of G (needed before calling care())
402+
_check_shape("QN", QN, G.shape[1], G.shape[1])
412403

413-
P, E, LT = care(A.T, C.T, G @ Q @ G.T, R, method=method)
404+
# Compute the result (dimension and symmetry checking done in care())
405+
P, E, LT = care(A.T, C.T, G @ QN @ G.T, RN, method=method,
406+
B_s="C", Q_s="QN", R_s="RN", S_s="NN")
414407
return _ssmatrix(LT.T), _ssmatrix(P), E
415408

416409

@@ -524,7 +517,9 @@ def dlqe(*args, **keywords):
524517
NN = np.array(args[index+2], ndmin=2, dtype=float)
525518
raise ControlNotImplemented("cross-covariance not yet implememented")
526519

527-
P, E, LT = dare(A.T, C.T, np.dot(np.dot(G, QN), G.T), RN, method=method)
520+
# Compute the result (dimension and symmetry checking done in dare())
521+
P, E, LT = dare(A.T, C.T, G @ QN @ G.T, RN, method=method,
522+
B_s="C", Q_s="QN", R_s="RN", S_s="NN")
528523
return _ssmatrix(LT.T), _ssmatrix(P), E
529524

530525
# Contributed by Roberto Bucher <roberto.bucher@supsi.ch>
@@ -675,8 +670,8 @@ def lqr(*args, **keywords):
675670
else:
676671
N = None
677672

678-
# Solve continuous algebraic Riccati equation
679-
X, L, G = care(A, B, Q, R, N, None, method=method)
673+
# Compute the result (dimension and symmetry checking done in care())
674+
X, L, G = care(A, B, Q, R, N, None, method=method, S_s="N")
680675
return G, X, L
681676

682677

@@ -771,19 +766,8 @@ def dlqr(*args, **keywords):
771766
else:
772767
N = np.zeros((Q.shape[0], R.shape[1]))
773768

774-
# Check dimensions for consistency
775-
nstates = B.shape[0]
776-
ninputs = B.shape[1]
777-
if (A.shape[0] != nstates or A.shape[1] != nstates):
778-
raise ControlDimension("inconsistent system dimensions")
779-
780-
elif (Q.shape[0] != nstates or Q.shape[1] != nstates or
781-
R.shape[0] != ninputs or R.shape[1] != ninputs or
782-
N.shape[0] != nstates or N.shape[1] != ninputs):
783-
raise ControlDimension("incorrect weighting matrix dimensions")
784-
785-
# compute the result
786-
S, E, K = dare(A, B, Q, R, N, method=method)
769+
# Compute the result (dimension and symmetry checking done in dare())
770+
S, E, K = dare(A, B, Q, R, N, method=method, S_s="N")
787771
return _ssmatrix(K), _ssmatrix(S), E
788772

789773

control/tests/statefbk_test.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -472,11 +472,11 @@ def test_lqe_call_format(self):
472472
L, P, E = lqe(sys, Q, R, N)
473473

474474
# Inconsistent system dimensions
475-
with pytest.raises(ct.ControlDimension, match="inconsistent system"):
475+
with pytest.raises(ct.ControlDimension, match="Incompatible"):
476476
L, P, E = lqe(sys.A, sys.C, sys.B, Q, R)
477477

478478
# incorrect covariance matrix dimensions
479-
with pytest.raises(ct.ControlDimension, match="incorrect covariance"):
479+
with pytest.raises(ct.ControlDimension, match="Incompatible"):
480480
L, P, E = lqe(sys.A, sys.B, sys.C, R, Q)
481481

482482
def check_DLQE(self, L, P, poles, G, QN, RN):

0 commit comments

Comments
 (0)