diff --git a/control/mateqn.py b/control/mateqn.py index b73abdfcc..52b69e2b0 100644 --- a/control/mateqn.py +++ b/control/mateqn.py @@ -44,7 +44,6 @@ from .exception import ControlSlycot, ControlArgument, ControlDimension, \ slycot_check -from .statesp import _ssmatrix # Make sure we have access to the right slycot routines try: @@ -151,12 +150,12 @@ def lyap(A, Q, C=None, E=None, method=None): m = Q.shape[0] # Check to make sure input matrices are the right shape and type - _check_shape("A", A, n, n, square=True) + _check_shape(A, n, n, square=True, name="A") # Solve standard Lyapunov equation if C is None and E is None: # Check to make sure input matrices are the right shape and type - _check_shape("Q", Q, n, n, square=True, symmetric=True) + _check_shape(Q, n, n, square=True, symmetric=True, name="Q") if method == 'scipy': # Solve the Lyapunov equation using SciPy @@ -171,8 +170,8 @@ def lyap(A, Q, C=None, E=None, method=None): # Solve the Sylvester equation elif C is not None and E is None: # Check to make sure input matrices are the right shape and type - _check_shape("Q", Q, m, m, square=True) - _check_shape("C", C, n, m) + _check_shape(Q, m, m, square=True, name="Q") + _check_shape(C, n, m, name="C") if method == 'scipy': # Solve the Sylvester equation using SciPy @@ -184,8 +183,8 @@ def lyap(A, Q, C=None, E=None, method=None): # Solve the generalized Lyapunov equation elif C is None and E is not None: # Check to make sure input matrices are the right shape and type - _check_shape("Q", Q, n, n, square=True, symmetric=True) - _check_shape("E", E, n, n, square=True) + _check_shape(Q, n, n, square=True, symmetric=True, name="Q") + _check_shape(E, n, n, square=True, name="E") if method == 'scipy': raise ControlArgument( @@ -210,7 +209,7 @@ def lyap(A, Q, C=None, E=None, method=None): else: raise ControlArgument("Invalid set of input parameters") - return _ssmatrix(X) + return X def dlyap(A, Q, C=None, E=None, method=None): @@ -281,12 +280,12 @@ def dlyap(A, Q, C=None, E=None, method=None): m = Q.shape[0] # Check to make sure input matrices are the right shape and type - _check_shape("A", A, n, n, square=True) + _check_shape(A, n, n, square=True, name="A") # Solve standard Lyapunov equation if C is None and E is None: # Check to make sure input matrices are the right shape and type - _check_shape("Q", Q, n, n, square=True, symmetric=True) + _check_shape(Q, n, n, square=True, symmetric=True, name="Q") if method == 'scipy': # Solve the Lyapunov equation using SciPy @@ -301,8 +300,8 @@ def dlyap(A, Q, C=None, E=None, method=None): # Solve the Sylvester equation elif C is not None and E is None: # Check to make sure input matrices are the right shape and type - _check_shape("Q", Q, m, m, square=True) - _check_shape("C", C, n, m) + _check_shape(Q, m, m, square=True, name="Q") + _check_shape(C, n, m, name="C") if method == 'scipy': raise ControlArgument( @@ -314,8 +313,8 @@ def dlyap(A, Q, C=None, E=None, method=None): # Solve the generalized Lyapunov equation elif C is None and E is not None: # Check to make sure input matrices are the right shape and type - _check_shape("Q", Q, n, n, square=True, symmetric=True) - _check_shape("E", E, n, n, square=True) + _check_shape(Q, n, n, square=True, symmetric=True, name="Q") + _check_shape(E, n, n, square=True, name="E") if method == 'scipy': raise ControlArgument( @@ -333,7 +332,7 @@ def dlyap(A, Q, C=None, E=None, method=None): else: raise ControlArgument("Invalid set of input parameters") - return _ssmatrix(X) + return X # @@ -407,10 +406,10 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, m = B.shape[1] # Check to make sure input matrices are the right shape and type - _check_shape(_As, A, n, n, square=True) - _check_shape(_Bs, B, n, m) - _check_shape(_Qs, Q, n, n, square=True, symmetric=True) - _check_shape(_Rs, R, m, m, square=True, symmetric=True) + _check_shape(A, n, n, square=True, name=_As) + _check_shape(B, n, m, name=_Bs) + _check_shape(Q, n, n, square=True, symmetric=True, name=_Qs) + _check_shape(R, m, m, square=True, symmetric=True, name=_Rs) # Solve the standard algebraic Riccati equation 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, X = sp.linalg.solve_continuous_are(A, B, Q, R) K = np.linalg.solve(R, B.T @ X) E, _ = np.linalg.eig(A - B @ K) - return _ssmatrix(X), E, _ssmatrix(K) + return X, E, K # Make sure we can import required slycot routines try: @@ -448,7 +447,7 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, # Return the solution X, the closed-loop eigenvalues L and # the gain matrix G - return _ssmatrix(X), w[:n], _ssmatrix(G) + return X, w[:n], G # Solve the generalized algebraic Riccati equation else: @@ -457,8 +456,8 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, E = np.eye(A.shape[0]) if E is None else np.array(E, ndmin=2) # Check to make sure input matrices are the right shape and type - _check_shape(_Es, E, n, n, square=True) - _check_shape(_Ss, S, n, m) + _check_shape(E, n, n, square=True, name=_Es) + _check_shape(S, n, m, name=_Ss) # See if we should solve this using SciPy if method == 'scipy': @@ -469,7 +468,7 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, X = sp.linalg.solve_continuous_are(A, B, Q, R, s=S, e=E) K = np.linalg.solve(R, B.T @ X @ E + S.T) eigs, _ = sp.linalg.eig(A - B @ K, E) - return _ssmatrix(X), eigs, _ssmatrix(K) + return X, eigs, K # Make sure we can find the required slycot routine try: @@ -494,7 +493,7 @@ def care(A, B, Q, R=None, S=None, E=None, stabilizing=True, method=None, # Return the solution X, the closed-loop eigenvalues L and # the gain matrix G - return _ssmatrix(X), L, _ssmatrix(G) + return X, L, G def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None, _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, m = B.shape[1] # Check to make sure input matrices are the right shape and type - _check_shape(_As, A, n, n, square=True) - _check_shape(_Bs, B, n, m) - _check_shape(_Qs, Q, n, n, square=True, symmetric=True) - _check_shape(_Rs, R, m, m, square=True, symmetric=True) + _check_shape(A, n, n, square=True, name=_As) + _check_shape(B, n, m, name=_Bs) + _check_shape(Q, n, n, square=True, symmetric=True, name=_Qs) + _check_shape(R, m, m, square=True, symmetric=True, name=_Rs) if E is not None: - _check_shape(_Es, E, n, n, square=True) + _check_shape(E, n, n, square=True, name=_Es) if S is not None: - _check_shape(_Ss, S, n, m) + _check_shape(S, n, m, name=_Ss) # Figure out how to solve the problem if method == 'scipy': @@ -589,7 +588,7 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None, else: L, _ = sp.linalg.eig(A - B @ G, E) - return _ssmatrix(X), L, _ssmatrix(G) + return X, L, G # Make sure we can import required slycot routine try: @@ -618,7 +617,7 @@ def dare(A, B, Q, R, S=None, E=None, stabilizing=True, method=None, # Return the solution X, the closed-loop eigenvalues L and # the gain matrix G - return _ssmatrix(X), L, _ssmatrix(G) + return X, L, G # Utility function to decide on method to use @@ -632,7 +631,7 @@ def _slycot_or_scipy(method): # Utility function to check matrix dimensions -def _check_shape(name, M, n, m, square=False, symmetric=False): +def _check_shape(M, n, m, square=False, symmetric=False, name="??"): if square and M.shape[0] != M.shape[1]: raise ControlDimension("%s must be a square matrix" % name) @@ -640,7 +639,9 @@ def _check_shape(name, M, n, m, square=False, symmetric=False): raise ControlArgument("%s must be a symmetric matrix" % name) if M.shape[0] != n or M.shape[1] != m: - raise ControlDimension("Incompatible dimensions of %s matrix" % name) + raise ControlDimension( + f"Incompatible dimensions of {name} matrix; " + f"expected ({n}, {m}) but found {M.shape}") # Utility function to check if a matrix is symmetric diff --git a/control/statefbk.py b/control/statefbk.py index 7b96c8015..87b12da82 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -50,7 +50,7 @@ ControlSlycot from .iosys import _process_indices, _process_labels, isctime, isdtime from .lti import LTI -from .mateqn import _check_shape, care, dare +from .mateqn import care, dare from .nlsys import NonlinearIOSystem, interconnect from .statesp import StateSpace, _ssmatrix, ss @@ -130,21 +130,15 @@ def place(A, B, p): from scipy.signal import place_poles # Convert the system inputs to NumPy arrays - A_mat = np.array(A) - B_mat = np.array(B) - if (A_mat.shape[0] != A_mat.shape[1]): - raise ControlDimension("A must be a square matrix") - - if (A_mat.shape[0] != B_mat.shape[0]): - err_str = "The number of rows of A must equal the number of rows in B" - raise ControlDimension(err_str) + A_mat = _ssmatrix(A, square=True, name="A") + B_mat = _ssmatrix(B, axis=0, rows=A_mat.shape[0]) # Convert desired poles to numpy array placed_eigs = np.atleast_1d(np.squeeze(np.asarray(p))) result = place_poles(A_mat, B_mat, placed_eigs, method='YT') K = result.gain_matrix - return _ssmatrix(K) + return K def place_varga(A, B, p, dtime=False, alpha=None): @@ -206,10 +200,8 @@ def place_varga(A, B, p, dtime=False, alpha=None): raise ControlSlycot("can't find slycot module 'sb01bd'") # Convert the system inputs to NumPy arrays - A_mat = np.array(A) - B_mat = np.array(B) - if (A_mat.shape[0] != A_mat.shape[1] or A_mat.shape[0] != B_mat.shape[0]): - raise ControlDimension("matrix dimensions are incorrect") + A_mat = _ssmatrix(A, square=True, name="A") + B_mat = _ssmatrix(B, axis=0, rows=A_mat.shape[0]) # Compute the system eigenvalues and convert poles to numpy array system_eigs = np.linalg.eig(A_mat)[0] @@ -246,7 +238,7 @@ def place_varga(A, B, p, dtime=False, alpha=None): A_mat, B_mat, placed_eigs, DICO) # Return the gain matrix, with MATLAB gain convention - return _ssmatrix(-F) + return -F # Contributed by Roberto Bucher @@ -274,12 +266,12 @@ def acker(A, B, poles): """ # Convert the inputs to matrices - a = _ssmatrix(A) - b = _ssmatrix(B) + A = _ssmatrix(A, square=True, name="A") + B = _ssmatrix(B, axis=0, rows=A.shape[0], name="B") # Make sure the system is controllable ct = ctrb(A, B) - if np.linalg.matrix_rank(ct) != a.shape[0]: + if np.linalg.matrix_rank(ct) != A.shape[0]: raise ValueError("System not reachable; pole placement invalid") # Compute the desired characteristic polynomial @@ -288,13 +280,13 @@ def acker(A, B, poles): # Place the poles using Ackermann's method # TODO: compute pmat using Horner's method (O(n) instead of O(n^2)) n = np.size(p) - pmat = p[n-1] * np.linalg.matrix_power(a, 0) + pmat = p[n-1] * np.linalg.matrix_power(A, 0) for i in np.arange(1, n): - pmat = pmat + p[n-i-1] * np.linalg.matrix_power(a, i) + pmat = pmat + p[n-i-1] * np.linalg.matrix_power(A, i) K = np.linalg.solve(ct, pmat) - K = K[-1][:] # Extract the last row - return _ssmatrix(K) + K = K[-1, :] # Extract the last row + return K def lqr(*args, **kwargs): @@ -577,7 +569,7 @@ def dlqr(*args, **kwargs): # Compute the result (dimension and symmetry checking done in dare()) S, E, K = dare(A, B, Q, R, N, method=method, _Ss="N") - return _ssmatrix(K), _ssmatrix(S), E + return K, S, E # Function to create an I/O sytems representing a state feedback controller @@ -1098,17 +1090,11 @@ def ctrb(A, B, t=None): """ # Convert input parameters to matrices (if they aren't already) - A = _ssmatrix(A) - if np.asarray(B).ndim == 1 and len(B) == A.shape[0]: - B = _ssmatrix(B, axis=0) - else: - B = _ssmatrix(B) - + A = _ssmatrix(A, square=True, name="A") n = A.shape[0] - m = B.shape[1] - _check_shape('A', A, n, n, square=True) - _check_shape('B', B, n, m) + B = _ssmatrix(B, axis=0, rows=n, name="B") + m = B.shape[1] if t is None or t > n: t = n @@ -1119,7 +1105,7 @@ def ctrb(A, B, t=None): for k in range(1, t): ctrb[:, k * m:(k + 1) * m] = np.dot(A, ctrb[:, (k - 1) * m:k * m]) - return _ssmatrix(ctrb) + return ctrb def obsv(A, C, t=None): @@ -1145,16 +1131,12 @@ def obsv(A, C, t=None): np.int64(2) """ - # Convert input parameters to matrices (if they aren't already) - A = _ssmatrix(A) - C = _ssmatrix(C) - - n = np.shape(A)[0] - p = np.shape(C)[0] + A = _ssmatrix(A, square=True, name="A") + n = A.shape[0] - _check_shape('A', A, n, n, square=True) - _check_shape('C', C, p, n) + C = _ssmatrix(C, cols=n, name="C") + p = C.shape[0] if t is None or t > n: t = n @@ -1166,7 +1148,7 @@ def obsv(A, C, t=None): for k in range(1, t): obsv[k * p:(k + 1) * p, :] = np.dot(obsv[(k - 1) * p:k * p, :], A) - return _ssmatrix(obsv) + return obsv def gram(sys, type): @@ -1246,7 +1228,7 @@ def gram(sys, type): X, scale, sep, ferr, w = sb03md( n, C, A, U, dico, job='X', fact='N', trana=tra) gram = X - return _ssmatrix(gram) + return gram elif type == 'cf' or type == 'of': # Compute cholesky factored gramian from slycot routine sb03od @@ -1269,4 +1251,4 @@ def gram(sys, type): X, scale, w = sb03od( n, m, A, Q, C.transpose(), dico, fact='N', trans=tra) gram = X - return _ssmatrix(gram) + return gram diff --git a/control/statesp.py b/control/statesp.py index 44fe8b605..c0e8e7862 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -31,12 +31,14 @@ from . import config from . import bdalg -from .exception import ControlMIMONotImplemented, ControlSlycot, slycot_check +from .exception import ControlDimension, ControlMIMONotImplemented, \ + ControlSlycot, slycot_check from .frdata import FrequencyResponseData from .iosys import InputOutputSystem, NamedSignal, _process_dt_keyword, \ _process_iosys_keywords, _process_signal_list, _process_subsys_index, \ common_timebase, iosys_repr, isdtime, issiso from .lti import LTI, _process_frequency_response +from .mateqn import _check_shape from .nlsys import InterconnectedSystem, NonlinearIOSystem import control @@ -199,21 +201,18 @@ def __init__(self, *args, **kwargs): raise TypeError( "Expected 1, 4, or 5 arguments; received %i." % len(args)) - # Convert all matrices to standard form - A = _ssmatrix(A) - # if B is a 1D array, turn it into a column vector if it fits - if np.asarray(B).ndim == 1 and len(B) == A.shape[0]: - B = _ssmatrix(B, axis=0) - else: - B = _ssmatrix(B) - if np.asarray(C).ndim == 1 and len(C) == A.shape[0]: - C = _ssmatrix(C, axis=1) - else: - C = _ssmatrix(C, axis=0) # if this doesn't work, error below + # Convert all matrices to standard form (sizes checked later) + A = _ssmatrix(A, square=True, name="A") + B = _ssmatrix( + B, axis=0 if np.asarray(B).ndim == 1 and len(B) == A.shape[0] + else 1, name="B") + C = _ssmatrix( + C, axis=1 if np.asarray(C).ndim == 1 and len(C) == A.shape[0] + else 0, name="C") if np.isscalar(D) and D == 0 and B.shape[1] > 0 and C.shape[0] > 0: # If D is a scalar zero, broadcast it to the proper size D = np.zeros((C.shape[0], B.shape[1])) - D = _ssmatrix(D) + D = _ssmatrix(D, name="D") # If only direct term is present, adjust sizes of C and D if needed if D.size > 0 and B.size == 0: @@ -260,19 +259,11 @@ def __init__(self, *args, **kwargs): B.shape = (0, self.ninputs) C.shape = (self.noutputs, 0) - # # Check to make sure everything is consistent - # - # Check that the matrix sizes are consistent - def _check_shape(matrix, expected, name): - if matrix.shape != expected: - raise ValueError( - f"{name} is the wrong shape; " - f"expected {expected} instead of {matrix.shape}") - _check_shape(A, (self.nstates, self.nstates), "A") - _check_shape(B, (self.nstates, self.ninputs), "B") - _check_shape(C, (self.noutputs, self.nstates), "C") - _check_shape(D, (self.noutputs, self.ninputs), "D") + _check_shape(A, self.nstates, self.nstates, name="A") + _check_shape(B, self.nstates, self.ninputs, name="B") + _check_shape(C, self.noutputs, self.nstates, name="C") + _check_shape(D, self.noutputs, self.ninputs, name="D") # # Final processing @@ -2230,11 +2221,11 @@ def _parse_list(signals, signame='input', prefix='u'): # Utility functions # -def _ssmatrix(data, axis=1): +def _ssmatrix(data, axis=1, square=None, rows=None, cols=None, name=None): """Convert argument to a (possibly empty) 2D state space matrix. - The axis keyword argument makes it convenient to specify that if the input - is a vector, it is a row (axis=1) or column (axis=0) vector. + The axis keyword argument makes it convenient to specify that if the + input is a vector, it is a row (axis=1) or column (axis=0) vector. Parameters ---------- @@ -2243,20 +2234,31 @@ def _ssmatrix(data, axis=1): axis : 0 or 1 If input data is 1D, which axis to use for return object. The default is 1, corresponding to a row matrix. + square : bool, optional + If set to True, check that the input matrix is square. + rows : int, optional + If set, check that the input matrix has the given number of rows. + cols : int, optional + If set, check that the input matrix has the given number of columns. + name : str, optional + Name of the state-space matrix being checked (for error messages). Returns ------- arr : 2D array, with shape (0, 0) if a is empty """ - # Convert the data into an array + # Process the name of the object, if available + name = "" if name is None else " " + name + + # Convert the data into an array (always making a copy) arr = np.array(data, dtype=float) ndim = arr.ndim shape = arr.shape # Change the shape of the array into a 2D array if (ndim > 2): - raise ValueError("state-space matrix must be 2-dimensional") + raise ValueError(f"state-space matrix{name} must be 2-dimensional") elif (ndim == 2 and shape == (1, 0)) or \ (ndim == 1 and shape == (0, )): @@ -2271,6 +2273,21 @@ def _ssmatrix(data, axis=1): # Passed a constant; turn into a matrix shape = (1, 1) + # Check to make sure any conditions are satisfied + if square and shape[0] != shape[1]: + raise ControlDimension( + f"state-space matrix{name} must be a square matrix") + + if rows is not None and shape[0] != rows: + raise ControlDimension( + f"state-space matrix{name} has the wrong number of rows; " + f"expected {rows} instead of {shape[0]}") + + if cols is not None and shape[1] != cols: + raise ControlDimension( + f"state-space matrix{name} has the wrong number of columns; " + f"expected {cols} instead of {shape[1]}") + # Create the actual object used to store the result return arr.reshape(shape) @@ -2382,7 +2399,7 @@ def _convert_to_statespace(sys, use_prefix_suffix=False, method=None): # If this is a matrix, try to create a constant feedthrough try: - D = _ssmatrix(np.atleast_2d(sys)) + D = _ssmatrix(np.atleast_2d(sys), name="D") return StateSpace([], [], [], D, dt=None) except Exception: diff --git a/control/stochsys.py b/control/stochsys.py index b31083f19..5aaa29415 100644 --- a/control/stochsys.py +++ b/control/stochsys.py @@ -26,7 +26,6 @@ _process_labels, _process_control_disturbance_indices from .nlsys import NonlinearIOSystem from .mateqn import care, dare, _check_shape -from .statesp import StateSpace, _ssmatrix from .exception import ControlArgument, ControlNotImplemented from .config import _process_legacy_keyword @@ -173,12 +172,12 @@ def lqe(*args, **kwargs): NN = np.zeros((QN.shape[0], RN.shape[1])) # Check dimensions of G (needed before calling care()) - _check_shape("QN", QN, G.shape[1], G.shape[1]) + _check_shape(QN, G.shape[1], G.shape[1], name="QN") # Compute the result (dimension and symmetry checking done in care()) P, E, LT = care(A.T, C.T, G @ QN @ G.T, RN, method=method, _Bs="C", _Qs="QN", _Rs="RN", _Ss="NN") - return _ssmatrix(LT.T), _ssmatrix(P), E + return LT.T, P, E # contributed by Sawyer B. Fuller @@ -293,12 +292,12 @@ def dlqe(*args, **kwargs): raise ControlNotImplemented("cross-covariance not yet implememented") # Check dimensions of G (needed before calling care()) - _check_shape("QN", QN, G.shape[1], G.shape[1]) + _check_shape(QN, G.shape[1], G.shape[1], name="QN") # Compute the result (dimension and symmetry checking done in dare()) P, E, LT = dare(A.T, C.T, G @ QN @ G.T, RN, method=method, _Bs="C", _Qs="QN", _Rs="RN", _Ss="NN") - return _ssmatrix(LT.T), _ssmatrix(P), E + return LT.T, P, E # Function to create an estimator diff --git a/control/tests/docstrings_test.py b/control/tests/docstrings_test.py index b1fce53e0..4dbf52ee8 100644 --- a/control/tests/docstrings_test.py +++ b/control/tests/docstrings_test.py @@ -30,9 +30,9 @@ function_docstring_hash = { control.append: '1bddbac0fe932755c85e9fb0bfb97d88', control.describing_function_plot: '95f894706b1d3eeb3b854934596af09f', - control.dlqe: '9db995ed95c2214ce97074b0616a3191', - control.dlqr: '896cfa651dbbd80e417635904d13c9d6', - control.lqe: '567bf657538935173f2e50700ba87168', + control.dlqe: 'f2e52e35692cf5ffe911684d41d284c9', + control.dlqr: '56d7f3a452bc8d7a7256a52d9d1dcb37', + control.lqe: 'f0ba6cde8191cbc10f052096ffc3fcbb', control.lqr: 'a3e0a85f781fc9c0f69a4b7da4f0bd22', control.margin: 'f02b3034f5f1d44ce26f916cc3e51600', control.parallel: 'bfc470aef75dbb923f9c6fb8bf3c9b43', diff --git a/control/tests/statefbk_test.py b/control/tests/statefbk_test.py index 110c8dbc7..0714e6f3b 100644 --- a/control/tests/statefbk_test.py +++ b/control/tests/statefbk_test.py @@ -67,11 +67,14 @@ def testCtrbNdim1(self): def testCtrbRejectMismatch(self): # gh-1097: check A, B for compatible shapes - with pytest.raises(ControlDimension, match='A must be a square matrix'): + with pytest.raises( + ControlDimension, match='.* A must be a square matrix'): ctrb([[1,2]],[1]) - with pytest.raises(ControlDimension, match='Incompatible dimensions of B matrix'): + with pytest.raises( + ControlDimension, match='B has the wrong number of rows'): ctrb([[1,2],[2,3]], 1) - with pytest.raises(ControlDimension, match='Incompatible dimensions of B matrix'): + with pytest.raises( + ControlDimension, match='B has the wrong number of rows'): ctrb([[1,2],[2,3]], [[1,2]]) def testObsvSISO(self): @@ -106,11 +109,14 @@ def testObsvNdim1(self): def testObsvRejectMismatch(self): # gh-1097: check A, C for compatible shapes - with pytest.raises(ControlDimension, match='A must be a square matrix'): + with pytest.raises( + ControlDimension, match='.* A must be a square matrix'): obsv([[1,2]],[1]) - with pytest.raises(ControlDimension, match='Incompatible dimensions of C matrix'): + with pytest.raises( + ControlDimension, match='C has the wrong number of columns'): obsv([[1,2],[2,3]], 1) - with pytest.raises(ControlDimension, match='Incompatible dimensions of C matrix'): + with pytest.raises( + ControlDimension, match='C has the wrong number of columns'): obsv([[1,2],[2,3]], [[1],[2]]) def testCtrbObsvDuality(self): diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index 6d57a38a9..5325cb3cc 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -127,19 +127,16 @@ def test_constructor(self, sys322ABCD, dt, argfun): ((1, 2), TypeError, "1, 4, or 5 arguments"), ((np.ones((3, 2)), np.ones((3, 2)), np.ones((2, 2)), np.ones((2, 2))), ValueError, - r"A is the wrong shape; expected \(3, 3\)"), + r"A must be a square matrix"), ((np.ones((3, 3)), np.ones((2, 2)), np.ones((2, 3)), np.ones((2, 2))), ValueError, - r"B is the wrong shape; expected \(3, 2\)"), + r"Incompatible dimensions of B matrix; expected \(3, 2\)"), ((np.ones((3, 3)), np.ones((3, 2)), np.ones((2, 2)), np.ones((2, 2))), ValueError, - r"C is the wrong shape; expected \(2, 3\)"), + r"Incompatible dimensions of C matrix; expected \(2, 3\)"), ((np.ones((3, 3)), np.ones((3, 2)), np.ones((2, 3)), np.ones((2, 3))), ValueError, - r"D is the wrong shape; expected \(2, 2\)"), - ((np.ones((3, 3)), np.ones((3, 2)), - np.ones((2, 3)), np.ones((3, 2))), ValueError, - r"D is the wrong shape; expected \(2, 2\)"), + r"Incompatible dimensions of D matrix; expected \(2, 2\)"), ]) def test_constructor_invalid(self, args, exc, errmsg): """Test invalid input to StateSpace() constructor"""