Skip to content

Commit d2f9a9c

Browse files
authored
Merge pull request #1116 from murrayrm/update_ssmatrix-01Feb2025
Update _ssmatrix and _check_shape for consistent usage
2 parents 2cb0520 + 4fc70f7 commit d2f9a9c

File tree

7 files changed

+133
-131
lines changed

7 files changed

+133
-131
lines changed

control/mateqn.py

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444

4545
from .exception import ControlSlycot, ControlArgument, ControlDimension, \
4646
slycot_check
47-
from .statesp import _ssmatrix
4847

4948
# Make sure we have access to the right slycot routines
5049
try:
@@ -151,12 +150,12 @@ def lyap(A, Q, C=None, E=None, method=None):
151150
m = Q.shape[0]
152151

153152
# Check to make sure input matrices are the right shape and type
154-
_check_shape("A", A, n, n, square=True)
153+
_check_shape(A, n, n, square=True, name="A")
155154

156155
# Solve standard Lyapunov equation
157156
if C is None and E is None:
158157
# Check to make sure input matrices are the right shape and type
159-
_check_shape("Q", Q, n, n, square=True, symmetric=True)
158+
_check_shape(Q, n, n, square=True, symmetric=True, name="Q")
160159

161160
if method == 'scipy':
162161
# Solve the Lyapunov equation using SciPy
@@ -171,8 +170,8 @@ def lyap(A, Q, C=None, E=None, method=None):
171170
# Solve the Sylvester equation
172171
elif C is not None and E is None:
173172
# Check to make sure input matrices are the right shape and type
174-
_check_shape("Q", Q, m, m, square=True)
175-
_check_shape("C", C, n, m)
173+
_check_shape(Q, m, m, square=True, name="Q")
174+
_check_shape(C, n, m, name="C")
176175

177176
if method == 'scipy':
178177
# Solve the Sylvester equation using SciPy
@@ -184,8 +183,8 @@ def lyap(A, Q, C=None, E=None, method=None):
184183
# Solve the generalized Lyapunov equation
185184
elif C is None and E is not None:
186185
# Check to make sure input matrices are the right shape and type
187-
_check_shape("Q", Q, n, n, square=True, symmetric=True)
188-
_check_shape("E", E, n, n, square=True)
186+
_check_shape(Q, n, n, square=True, symmetric=True, name="Q")
187+
_check_shape(E, n, n, square=True, name="E")
189188

190189
if method == 'scipy':
191190
raise ControlArgument(
@@ -210,7 +209,7 @@ def lyap(A, Q, C=None, E=None, method=None):
210209
else:
211210
raise ControlArgument("Invalid set of input parameters")
212211

213-
return _ssmatrix(X)
212+
return X
214213

215214

216215
def dlyap(A, Q, C=None, E=None, method=None):
@@ -281,12 +280,12 @@ def dlyap(A, Q, C=None, E=None, method=None):
281280
m = Q.shape[0]
282281

283282
# Check to make sure input matrices are the right shape and type
284-
_check_shape("A", A, n, n, square=True)
283+
_check_shape(A, n, n, square=True, name="A")
285284

286285
# Solve standard Lyapunov equation
287286
if C is None and E is None:
288287
# Check to make sure input matrices are the right shape and type
289-
_check_shape("Q", Q, n, n, square=True, symmetric=True)
288+
_check_shape(Q, n, n, square=True, symmetric=True, name="Q")
290289

291290
if method == 'scipy':
292291
# Solve the Lyapunov equation using SciPy
@@ -301,8 +300,8 @@ def dlyap(A, Q, C=None, E=None, method=None):
301300
# Solve the Sylvester equation
302301
elif C is not None and E is None:
303302
# Check to make sure input matrices are the right shape and type
304-
_check_shape("Q", Q, m, m, square=True)
305-
_check_shape("C", C, n, m)
303+
_check_shape(Q, m, m, square=True, name="Q")
304+
_check_shape(C, n, m, name="C")
306305

307306
if method == 'scipy':
308307
raise ControlArgument(
@@ -314,8 +313,8 @@ def dlyap(A, Q, C=None, E=None, method=None):
314313
# Solve the generalized Lyapunov equation
315314
elif C is None and E is not None:
316315
# Check to make sure input matrices are the right shape and type
317-
_check_shape("Q", Q, n, n, square=True, symmetric=True)
318-
_check_shape("E", E, n, n, square=True)
316+
_check_shape(Q, n, n, square=True, symmetric=True, name="Q")
317+
_check_shape(E, n, n, square=True, name="E")
319318

320319
if method == 'scipy':
321320
raise ControlArgument(
@@ -333,7 +332,7 @@ def dlyap(A, Q, C=None, E=None, method=None):
333332
else:
334333
raise ControlArgument("Invalid set of input parameters")
335334

336-
return _ssmatrix(X)
335+
return X
337336

338337

339338
#
@@ -407,10 +406,10 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None,
407406
m = B.shape[1]
408407

409408
# Check to make sure input matrices are the right shape and type
410-
_check_shape(_As, A, n, n, square=True)
411-
_check_shape(_Bs, B, n, m)
412-
_check_shape(_Qs, Q, n, n, square=True, symmetric=True)
413-
_check_shape(_Rs, R, m, m, square=True, symmetric=True)
409+
_check_shape(A, n, n, square=True, name=_As)
410+
_check_shape(B, n, m, name=_Bs)
411+
_check_shape(Q, n, n, square=True, symmetric=True, name=_Qs)
412+
_check_shape(R, m, m, square=True, symmetric=True, name=_Rs)
414413

415414
# Solve the standard algebraic Riccati equation
416415
if S is None and E is None:
@@ -423,7 +422,7 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None,
423422
X = sp.linalg.solve_continuous_are(A, B, Q, R)
424423
K = np.linalg.solve(R, B.T @ X)
425424
E, _ = np.linalg.eig(A - B @ K)
426-
return _ssmatrix(X), E, _ssmatrix(K)
425+
return X, E, K
427426

428427
# Make sure we can import required slycot routines
429428
try:
@@ -448,7 +447,7 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None,
448447

449448
# Return the solution X, the closed-loop eigenvalues L and
450449
# the gain matrix G
451-
return _ssmatrix(X), w[:n], _ssmatrix(G)
450+
return X, w[:n], G
452451

453452
# Solve the generalized algebraic Riccati equation
454453
else:
@@ -457,8 +456,8 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None,
457456
E = np.eye(A.shape[0]) if E is None else np.array(E, ndmin=2)
458457

459458
# Check to make sure input matrices are the right shape and type
460-
_check_shape(_Es, E, n, n, square=True)
461-
_check_shape(_Ss, S, n, m)
459+
_check_shape(E, n, n, square=True, name=_Es)
460+
_check_shape(S, n, m, name=_Ss)
462461

463462
# See if we should solve this using SciPy
464463
if method == 'scipy':
@@ -469,7 +468,7 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None,
469468
X = sp.linalg.solve_continuous_are(A, B, Q, R, s=S, e=E)
470469
K = np.linalg.solve(R, B.T @ X @ E + S.T)
471470
eigs, _ = sp.linalg.eig(A - B @ K, E)
472-
return _ssmatrix(X), eigs, _ssmatrix(K)
471+
return X, eigs, K
473472

474473
# Make sure we can find the required slycot routine
475474
try:
@@ -494,7 +493,7 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None,
494493

495494
# Return the solution X, the closed-loop eigenvalues L and
496495
# the gain matrix G
497-
return _ssmatrix(X), L, _ssmatrix(G)
496+
return X, L, G
498497

499498
def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None,
500499
_As="A", _Bs="B", _Qs="Q", _Rs="R", _Ss="S", _Es="E"):
@@ -564,14 +563,14 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None,
564563
m = B.shape[1]
565564

566565
# Check to make sure input matrices are the right shape and type
567-
_check_shape(_As, A, n, n, square=True)
568-
_check_shape(_Bs, B, n, m)
569-
_check_shape(_Qs, Q, n, n, square=True, symmetric=True)
570-
_check_shape(_Rs, R, m, m, square=True, symmetric=True)
566+
_check_shape(A, n, n, square=True, name=_As)
567+
_check_shape(B, n, m, name=_Bs)
568+
_check_shape(Q, n, n, square=True, symmetric=True, name=_Qs)
569+
_check_shape(R, m, m, square=True, symmetric=True, name=_Rs)
571570
if E is not None:
572-
_check_shape(_Es, E, n, n, square=True)
571+
_check_shape(E, n, n, square=True, name=_Es)
573572
if S is not None:
574-
_check_shape(_Ss, S, n, m)
573+
_check_shape(S, n, m, name=_Ss)
575574

576575
# Figure out how to solve the problem
577576
if method == 'scipy':
@@ -589,7 +588,7 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None,
589588
else:
590589
L, _ = sp.linalg.eig(A - B @ G, E)
591590

592-
return _ssmatrix(X), L, _ssmatrix(G)
591+
return X, L, G
593592

594593
# Make sure we can import required slycot routine
595594
try:
@@ -618,7 +617,7 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None,
618617

619618
# Return the solution X, the closed-loop eigenvalues L and
620619
# the gain matrix G
621-
return _ssmatrix(X), L, _ssmatrix(G)
620+
return X, L, G
622621

623622

624623
# Utility function to decide on method to use
@@ -632,15 +631,17 @@ def _slycot_or_scipy(method):
632631

633632

634633
# Utility function to check matrix dimensions
635-
def _check_shape(name, M, n, m, square=False, symmetric=False):
634+
def _check_shape(M, n, m, square=False, symmetric=False, name="??"):
636635
if square and M.shape[0] != M.shape[1]:
637636
raise ControlDimension("%s must be a square matrix" % name)
638637

639638
if symmetric and not _is_symmetric(M):
640639
raise ControlArgument("%s must be a symmetric matrix" % name)
641640

642641
if M.shape[0] != n or M.shape[1] != m:
643-
raise ControlDimension("Incompatible dimensions of %s matrix" % name)
642+
raise ControlDimension(
643+
f"Incompatible dimensions of {name} matrix; "
644+
f"expected ({n}, {m}) but found {M.shape}")
644645

645646

646647
# Utility function to check if a matrix is symmetric

control/statefbk.py

Lines changed: 26 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
ControlSlycot
5151
from .iosys import _process_indices, _process_labels, isctime, isdtime
5252
from .lti import LTI
53-
from .mateqn import _check_shape, care, dare
53+
from .mateqn import care, dare
5454
from .nlsys import NonlinearIOSystem, interconnect
5555
from .statesp import StateSpace, _ssmatrix, ss
5656

@@ -130,21 +130,15 @@ def place(A, B, p):
130130
from scipy.signal import place_poles
131131

132132
# Convert the system inputs to NumPy arrays
133-
A_mat = np.array(A)
134-
B_mat = np.array(B)
135-
if (A_mat.shape[0] != A_mat.shape[1]):
136-
raise ControlDimension("A must be a square matrix")
137-
138-
if (A_mat.shape[0] != B_mat.shape[0]):
139-
err_str = "The number of rows of A must equal the number of rows in B"
140-
raise ControlDimension(err_str)
133+
A_mat = _ssmatrix(A, square=True, name="A")
134+
B_mat = _ssmatrix(B, axis=0, rows=A_mat.shape[0])
141135

142136
# Convert desired poles to numpy array
143137
placed_eigs = np.atleast_1d(np.squeeze(np.asarray(p)))
144138

145139
result = place_poles(A_mat, B_mat, placed_eigs, method='YT')
146140
K = result.gain_matrix
147-
return _ssmatrix(K)
141+
return K
148142

149143

150144
def place_varga(A, B, p, dtime=False, alpha=None):
@@ -206,10 +200,8 @@ def place_varga(A, B, p, dtime=False, alpha=None):
206200
raise ControlSlycot("can't find slycot module 'sb01bd'")
207201

208202
# Convert the system inputs to NumPy arrays
209-
A_mat = np.array(A)
210-
B_mat = np.array(B)
211-
if (A_mat.shape[0] != A_mat.shape[1] or A_mat.shape[0] != B_mat.shape[0]):
212-
raise ControlDimension("matrix dimensions are incorrect")
203+
A_mat = _ssmatrix(A, square=True, name="A")
204+
B_mat = _ssmatrix(B, axis=0, rows=A_mat.shape[0])
213205

214206
# Compute the system eigenvalues and convert poles to numpy array
215207
system_eigs = np.linalg.eig(A_mat)[0]
@@ -246,7 +238,7 @@ def place_varga(A, B, p, dtime=False, alpha=None):
246238
A_mat, B_mat, placed_eigs, DICO)
247239

248240
# Return the gain matrix, with MATLAB gain convention
249-
return _ssmatrix(-F)
241+
return -F
250242

251243

252244
# Contributed by Roberto Bucher <roberto.bucher@supsi.ch>
@@ -274,12 +266,12 @@ def acker(A, B, poles):
274266
275267
"""
276268
# Convert the inputs to matrices
277-
a = _ssmatrix(A)
278-
b = _ssmatrix(B)
269+
A = _ssmatrix(A, square=True, name="A")
270+
B = _ssmatrix(B, axis=0, rows=A.shape[0], name="B")
279271

280272
# Make sure the system is controllable
281273
ct = ctrb(A, B)
282-
if np.linalg.matrix_rank(ct) != a.shape[0]:
274+
if np.linalg.matrix_rank(ct) != A.shape[0]:
283275
raise ValueError("System not reachable; pole placement invalid")
284276

285277
# Compute the desired characteristic polynomial
@@ -288,13 +280,13 @@ def acker(A, B, poles):
288280
# Place the poles using Ackermann's method
289281
# TODO: compute pmat using Horner's method (O(n) instead of O(n^2))
290282
n = np.size(p)
291-
pmat = p[n-1] * np.linalg.matrix_power(a, 0)
283+
pmat = p[n-1] * np.linalg.matrix_power(A, 0)
292284
for i in np.arange(1, n):
293-
pmat = pmat + p[n-i-1] * np.linalg.matrix_power(a, i)
285+
pmat = pmat + p[n-i-1] * np.linalg.matrix_power(A, i)
294286
K = np.linalg.solve(ct, pmat)
295287

296-
K = K[-1][:] # Extract the last row
297-
return _ssmatrix(K)
288+
K = K[-1, :] # Extract the last row
289+
return K
298290

299291

300292
def lqr(*args, **kwargs):
@@ -577,7 +569,7 @@ def dlqr(*args, **kwargs):
577569

578570
# Compute the result (dimension and symmetry checking done in dare())
579571
S, E, K = dare(A, B, Q, R, N, method=method, _Ss="N")
580-
return _ssmatrix(K), _ssmatrix(S), E
572+
return K, S, E
581573

582574

583575
# Function to create an I/O sytems representing a state feedback controller
@@ -1098,17 +1090,11 @@ def ctrb(A, B, t=None):
10981090
"""
10991091

11001092
# Convert input parameters to matrices (if they aren't already)
1101-
A = _ssmatrix(A)
1102-
if np.asarray(B).ndim == 1 and len(B) == A.shape[0]:
1103-
B = _ssmatrix(B, axis=0)
1104-
else:
1105-
B = _ssmatrix(B)
1106-
1093+
A = _ssmatrix(A, square=True, name="A")
11071094
n = A.shape[0]
1108-
m = B.shape[1]
11091095

1110-
_check_shape('A', A, n, n, square=True)
1111-
_check_shape('B', B, n, m)
1096+
B = _ssmatrix(B, axis=0, rows=n, name="B")
1097+
m = B.shape[1]
11121098

11131099
if t is None or t > n:
11141100
t = n
@@ -1119,7 +1105,7 @@ def ctrb(A, B, t=None):
11191105
for k in range(1, t):
11201106
ctrb[:, k * m:(k + 1) * m] = np.dot(A, ctrb[:, (k - 1) * m:k * m])
11211107

1122-
return _ssmatrix(ctrb)
1108+
return ctrb
11231109

11241110

11251111
def obsv(A, C, t=None):
@@ -1145,16 +1131,12 @@ def obsv(A, C, t=None):
11451131
np.int64(2)
11461132
11471133
"""
1148-
11491134
# Convert input parameters to matrices (if they aren't already)
1150-
A = _ssmatrix(A)
1151-
C = _ssmatrix(C)
1152-
1153-
n = np.shape(A)[0]
1154-
p = np.shape(C)[0]
1135+
A = _ssmatrix(A, square=True, name="A")
1136+
n = A.shape[0]
11551137

1156-
_check_shape('A', A, n, n, square=True)
1157-
_check_shape('C', C, p, n)
1138+
C = _ssmatrix(C, cols=n, name="C")
1139+
p = C.shape[0]
11581140

11591141
if t is None or t > n:
11601142
t = n
@@ -1166,7 +1148,7 @@ def obsv(A, C, t=None):
11661148
for k in range(1, t):
11671149
obsv[k * p:(k + 1) * p, :] = np.dot(obsv[(k - 1) * p:k * p, :], A)
11681150

1169-
return _ssmatrix(obsv)
1151+
return obsv
11701152

11711153

11721154
def gram(sys, type):
@@ -1246,7 +1228,7 @@ def gram(sys, type):
12461228
X, scale, sep, ferr, w = sb03md(
12471229
n, C, A, U, dico, job='X', fact='N', trana=tra)
12481230
gram = X
1249-
return _ssmatrix(gram)
1231+
return gram
12501232

12511233
elif type == 'cf' or type == 'of':
12521234
# Compute cholesky factored gramian from slycot routine sb03od
@@ -1269,4 +1251,4 @@ def gram(sys, type):
12691251
X, scale, w = sb03od(
12701252
n, m, A, Q, C.transpose(), dico, fact='N', trans=tra)
12711253
gram = X
1272-
return _ssmatrix(gram)
1254+
return gram

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy