diff --git a/control/statesp.py b/control/statesp.py index 2419b4e62..288b0831d 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -10,7 +10,7 @@ # Python 3 compatibility (needs to go here) from __future__ import print_function -from __future__ import division # for _convertToStateSpace +from __future__ import division # for _convertToStateSpace """Copyright (c) 2010 by California Institute of Technology All rights reserved. @@ -161,8 +161,8 @@ class StateSpace(LTI): A class for representing state-space models - The StateSpace class is used to represent state-space realizations of linear - time-invariant (LTI) systems: + The StateSpace class is used to represent state-space realizations of + linear time-invariant (LTI) systems: dx/dt = A x + B u y = C x + D u @@ -216,7 +216,6 @@ class StateSpace(LTI): # Allow ndarray * StateSpace to give StateSpace._rmul_() priority __array_priority__ = 11 # override ndarray and matrix types - def __init__(self, *args, **kwargs): """ StateSpace(A, B, C, D[, dt]) @@ -240,18 +239,21 @@ def __init__(self, *args, **kwargs): elif len(args) == 1: # Use the copy constructor. if not isinstance(args[0], StateSpace): - raise TypeError("The one-argument constructor can only take in a StateSpace " - "object. Received %s." % type(args[0])) + raise TypeError( + "The one-argument constructor can only take in a " + "StateSpace object. Received %s." % type(args[0])) A = args[0].A B = args[0].B C = args[0].C D = args[0].D else: - raise ValueError("Expected 1, 4, or 5 arguments; received %i." % len(args)) + raise ValueError( + "Expected 1, 4, or 5 arguments; received %i." % len(args)) # Process keyword arguments - remove_useless = kwargs.get('remove_useless', - config.defaults['statesp.remove_useless_states']) + remove_useless = kwargs.get( + 'remove_useless', + config.defaults['statesp.remove_useless_states']) # Convert all matrices to standard form A = _ssmatrix(A) @@ -263,7 +265,7 @@ def __init__(self, *args, **kwargs): 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 + C = _ssmatrix(C, axis=0) # if this doesn't work, error below 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])) @@ -302,9 +304,9 @@ def __init__(self, *args, **kwargs): if 0 == self.states: # static gain # matrix's default "empty" shape is 1x0 - A.shape = (0,0) - B.shape = (0,self.inputs) - C.shape = (self.outputs,0) + A.shape = (0, 0) + B.shape = (0, self.inputs) + C.shape = (self.outputs, 0) # Check that the matrix sizes are consistent. if self.states != A.shape[0]: @@ -319,14 +321,15 @@ def __init__(self, *args, **kwargs): raise ValueError("C and D must have the same number of rows.") # Check for states that don't do anything, and remove them. - if remove_useless: self._remove_useless_states() + if remove_useless: + self._remove_useless_states() def _remove_useless_states(self): """Check for states that don't do anything, and remove them. Scan the A, B, and C matrices for rows or columns of zeros. If the - zeros are such that a particular state has no effect on the input-output - dynamics, then remove that state from the A, B, and C matrices. + zeros are such that a particular state has no effect on the input- + output dynamics, then remove that state from the A, B, and C matrices. """ @@ -504,7 +507,8 @@ def _repr_latex_(self): return self._latex_separate() else: cfg = config.defaults['statesp.latex_repr_type'] - raise ValueError("Unknown statesp.latex_repr_type '{cfg}'".format(cfg=cfg)) + raise ValueError( + "Unknown statesp.latex_repr_type '{cfg}'".format(cfg=cfg)) # Negation of a system def __neg__(self): @@ -535,10 +539,9 @@ def __add__(self, other): # Concatenate the various arrays A = concatenate(( concatenate((self.A, zeros((self.A.shape[0], - other.A.shape[-1]))),axis=1), + other.A.shape[-1]))), axis=1), concatenate((zeros((other.A.shape[0], self.A.shape[-1])), - other.A),axis=1) - ),axis=0) + other.A), axis=1)), axis=0) B = concatenate((self.B, other.B), axis=0) C = concatenate((self.C, other.C), axis=1) D = self.D + other.D @@ -590,7 +593,7 @@ def __mul__(self, other): concatenate((np.dot(self.B, other.C), self.A), axis=1)), axis=0) B = concatenate((other.B, np.dot(self.B, other.D)), axis=0) - C = concatenate((np.dot(self.D, other.C), self.C),axis=1) + C = concatenate((np.dot(self.D, other.C), self.C), axis=1) D = np.dot(self.D, other.D) return StateSpace(A, B, C, D, dt) @@ -634,7 +637,8 @@ def __div__(self, other): def __rdiv__(self, other): """Right divide two LTI systems.""" - raise NotImplementedError("StateSpace.__rdiv__ is not implemented yet.") + raise NotImplementedError( + "StateSpace.__rdiv__ is not implemented yet.") def evalfr(self, omega): """Evaluate a SS system's transfer function at a single frequency. @@ -779,7 +783,8 @@ def zero(self): if nu == 0: return np.array([]) else: - return sp.linalg.eigvals(out[8][0:nu, 0:nu], out[9][0:nu, 0:nu]) + return sp.linalg.eigvals(out[8][0:nu, 0:nu], + out[9][0:nu, 0:nu]) except ImportError: # Slycot unavailable. Fall back to scipy. if self.C.shape[0] != self.D.shape[1]: @@ -801,7 +806,8 @@ def zero(self): concatenate((self.C, self.D), axis=1)), axis=0) M = pad(eye(self.A.shape[0]), ((0, self.C.shape[0]), (0, self.B.shape[1])), "constant") - return np.array([x for x in sp.linalg.eigvals(L, M, overwrite_a=True) + return np.array([x for x in sp.linalg.eigvals(L, M, + overwrite_a=True) if not isinf(x)]) # Feedback around a state space system @@ -812,8 +818,8 @@ def feedback(self, other=1, sign=-1): # Check to make sure the dimensions are OK if (self.inputs != other.outputs) or (self.outputs != other.inputs): - raise ValueError("State space systems don't have compatible inputs/outputs for " - "feedback.") + raise ValueError("State space systems don't have compatible " + "inputs/outputs for feedback.") dt = common_timebase(self.dt, other.dt) A1 = self.A @@ -827,7 +833,8 @@ def feedback(self, other=1, sign=-1): F = eye(self.inputs) - sign * np.dot(D2, D1) if matrix_rank(F) != self.inputs: - raise ValueError("I - sign * D2 * D1 is singular to working precision.") + raise ValueError( + "I - sign * D2 * D1 is singular to working precision.") # Precompute F\D2 and F\C2 (E = inv(F)) # We can solve two linear systems in one pass, since the @@ -916,16 +923,20 @@ def lft(self, other, nu=-1, ny=-1): # solve for the resulting ss by solving for [y, u] using [x, # xbar] and [w1, w2]. TH = np.linalg.solve(F, np.block( - [[C2, np.zeros((ny, other.states)), D21, np.zeros((ny, other.inputs - ny))], - [np.zeros((nu, self.states)), Cbar1, np.zeros((nu, self.inputs - nu)), Dbar12]] + [[C2, np.zeros((ny, other.states)), + D21, np.zeros((ny, other.inputs - ny))], + [np.zeros((nu, self.states)), Cbar1, + np.zeros((nu, self.inputs - nu)), Dbar12]] )) T11 = TH[:ny, :self.states] T12 = TH[:ny, self.states: self.states + other.states] T21 = TH[ny:, :self.states] T22 = TH[ny:, self.states: self.states + other.states] - H11 = TH[:ny, self.states + other.states: self.states + other.states + self.inputs - nu] + H11 = TH[:ny, self.states + other.states:self.states + + other.states + self.inputs - nu] H12 = TH[:ny, self.states + other.states + self.inputs - nu:] - H21 = TH[ny:, self.states + other.states: self.states + other.states + self.inputs - nu] + H21 = TH[ny:, self.states + other.states:self.states + + other.states + self.inputs - nu] H22 = TH[ny:, self.states + other.states + self.inputs - nu:] Ares = np.block([ @@ -956,13 +967,13 @@ def minreal(self, tol=0.0): try: from slycot import tb01pd B = empty((self.states, max(self.inputs, self.outputs))) - B[:,:self.inputs] = self.B + B[:, :self.inputs] = self.B C = empty((max(self.outputs, self.inputs), self.states)) - C[:self.outputs,:] = self.C + C[:self.outputs, :] = self.C A, B, C, nr = tb01pd(self.states, self.inputs, self.outputs, self.A, B, C, tol=tol) - return StateSpace(A[:nr,:nr], B[:nr,:self.inputs], - C[:self.outputs,:nr], self.D) + return StateSpace(A[:nr, :nr], B[:nr, :self.inputs], + C[:self.outputs, :nr], self.D) except ImportError: raise TypeError("minreal requires slycot tb01pd") else: @@ -1052,7 +1063,8 @@ def __getitem__(self, indices): raise IOError('must provide indices of length 2 for state space') i = indices[0] j = indices[1] - return StateSpace(self.A, self.B[:, j], self.C[i, :], self.D[i, j], self.dt) + return StateSpace(self.A, self.B[:, j], self.C[i, :], + self.D[i, j], self.dt) def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): """Convert a continuous time system to discrete time @@ -1104,9 +1116,9 @@ def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None): raise ValueError("System must be continuous time system") sys = (self.A, self.B, self.C, self.D) - if (method=='bilinear' or (method=='gbt' and alpha==0.5)) and \ + if (method == 'bilinear' or (method == 'gbt' and alpha == 0.5)) and \ prewarp_frequency is not None: - Twarp = 2*np.tan(prewarp_frequency*Ts/2)/prewarp_frequency + Twarp = 2 * np.tan(prewarp_frequency * Ts/2)/prewarp_frequency else: Twarp = Ts Ad, Bd, C, D, _ = cont2discrete(sys, Twarp, method, alpha) @@ -1133,7 +1145,8 @@ def dcgain(self): """ try: if self.isctime(): - gain = np.asarray(self.D-self.C.dot(np.linalg.solve(self.A, self.B))) + gain = np.asarray(self.D - + self.C.dot(np.linalg.solve(self.A, self.B))) else: gain = self.horner(1) except LinAlgError: @@ -1151,28 +1164,34 @@ def is_static_gain(self): def _convertToStateSpace(sys, **kw): """Convert a system to state space form (if needed). - If sys is already a state space, then it is returned. If sys is a transfer - function object, then it is converted to a state space and returned. If sys - is a scalar, then the number of inputs and outputs can be specified - manually, as in: + If sys is already a state space, then it is returned. If sys is a + transfer function object, then it is converted to a state space and + returned. If sys is a scalar, then the number of inputs and outputs can + be specified manually, as in: >>> sys = _convertToStateSpace(3.) # Assumes inputs = outputs = 1 >>> sys = _convertToStateSpace(1., inputs=3, outputs=2) In the latter example, A = B = C = 0 and D = [[1., 1., 1.] [1., 1., 1.]]. - """ from .xferfcn import TransferFunction import itertools + if isinstance(sys, StateSpace): if len(kw): - raise TypeError("If sys is a StateSpace, _convertToStateSpace \ -cannot take keywords.") + raise TypeError("If sys is a StateSpace, _convertToStateSpace " + "cannot take keywords.") # Already a state space system; just return it return sys + elif isinstance(sys, TransferFunction): + # Make sure the transfer function is proper + if any([[len(num) for num in col] for col in sys.num] > + [[len(num) for num in col] for col in sys.den]): + raise ValueError("Transfer function is non-proper; can't " + "convert to StateSpace system.") try: from slycot import td04ad if len(kw): @@ -1189,8 +1208,10 @@ def _convertToStateSpace(sys, **kw): denorder, den, num, tol=0) states = ssout[0] - return StateSpace(ssout[1][:states, :states], ssout[2][:states, :sys.inputs], - ssout[3][:sys.outputs, :states], ssout[4], sys.dt) + return StateSpace(ssout[1][:states, :states], + ssout[2][:states, :sys.inputs], + ssout[3][:sys.outputs, :states], ssout[4], + sys.dt) except ImportError: # No Slycot. Scipy tf->ss can't handle MIMO, but static # MIMO is an easy special case we can check for here @@ -1200,7 +1221,8 @@ def _convertToStateSpace(sys, **kw): for drow in sys.den) if 1 == maxn and 1 == maxd: D = empty((sys.outputs, sys.inputs), dtype=float) - for i, j in itertools.product(range(sys.outputs), range(sys.inputs)): + for i, j in itertools.product(range(sys.outputs), + range(sys.inputs)): D[i, j] = sys.num[i][j][0] / sys.den[i][j][0] return StateSpace([], [], [], D, sys.dt) else: @@ -1210,7 +1232,8 @@ def _convertToStateSpace(sys, **kw): # TODO: do we want to squeeze first and check dimenations? # I think this will fail if num and den aren't 1-D after # the squeeze - A, B, C, D = sp.signal.tf2ss(squeeze(sys.num), squeeze(sys.den)) + A, B, C, D = \ + sp.signal.tf2ss(squeeze(sys.num), squeeze(sys.den)) return StateSpace(A, B, C, D, sys.dt) elif isinstance(sys, (int, float, complex, np.number)): @@ -1227,18 +1250,19 @@ def _convertToStateSpace(sys, **kw): # The following Doesn't work due to inconsistencies in ltisys: # return StateSpace([[]], [[]], [[]], eye(outputs, inputs)) return StateSpace(0., zeros((1, inputs)), zeros((outputs, 1)), - sys * ones((outputs, inputs))) + sys * ones((outputs, inputs))) # If this is a matrix, try to create a constant feedthrough try: D = _ssmatrix(sys) return StateSpace([], [], [], D) except Exception as e: - print("Failure to assume argument is matrix-like in" \ - " _convertToStateSpace, result %s" % e) + print("Failure to assume argument is matrix-like in" + " _convertToStateSpace, result %s" % e) raise TypeError("Can't convert given type to StateSpace system.") + # TODO: add discrete time option def _rss_generate(states, inputs, outputs, type): """Generate a random state space. @@ -1264,13 +1288,13 @@ def _rss_generate(states, inputs, outputs, type): # Check for valid input arguments. if states < 1 or states % 1: raise ValueError("states must be a positive integer. states = %g." % - states) + states) if inputs < 1 or inputs % 1: raise ValueError("inputs must be a positive integer. inputs = %g." % - inputs) + inputs) if outputs < 1 or outputs % 1: raise ValueError("outputs must be a positive integer. outputs = %g." % - outputs) + outputs) # Make some poles for A. Preallocate a complex array. poles = zeros(states) + zeros(states) * 0.j @@ -1358,7 +1382,7 @@ def _rss_generate(states, inputs, outputs, type): # Convert a MIMO system to a SISO system # TODO: add discrete time check def _mimo2siso(sys, input, output, warn_conversion=False): - #pylint: disable=W0622 + # pylint: disable=W0622 """ Convert a MIMO system to a SISO system. (Convert a system with multiple inputs and/or outputs, to a system with a single input and output.) @@ -1398,7 +1422,7 @@ def _mimo2siso(sys, input, output, warn_conversion=False): "Selected output: {sel}, " "number of system outputs: {ext}." .format(sel=output, ext=sys.outputs)) - #Convert sys to SISO if necessary + # Convert sys to SISO if necessary if sys.inputs > 1 or sys.outputs > 1: if warn_conversion: warn("Converting MIMO system to SISO system. " @@ -1547,8 +1571,8 @@ def ss(*args, **kwargs): elif isinstance(sys, TransferFunction): return tf2ss(sys) else: - raise TypeError("ss(sys): sys must be a StateSpace or \ -TransferFunction object. It is %s." % type(sys)) + raise TypeError("ss(sys): sys must be a StateSpace or " + "TransferFunction object. It is %s." % type(sys)) else: raise ValueError("Needs 1, 4, or 5 arguments; received %i." % len(args)) @@ -1572,16 +1596,16 @@ def tf2ss(*args): Parameters ---------- - sys: LTI (StateSpace or TransferFunction) + sys : LTI (StateSpace or TransferFunction) A linear system - num: array_like, or list of list of array_like + num : array_like, or list of list of array_like Polynomial coefficients of the numerator - den: array_like, or list of list of array_like + den : array_like, or list of list of array_like Polynomial coefficients of the denominator Returns ------- - out: StateSpace + out : StateSpace New linear system in state space form Raises @@ -1618,8 +1642,8 @@ def tf2ss(*args): elif len(args) == 1: sys = args[0] if not isinstance(sys, TransferFunction): - raise TypeError("tf2ss(sys): sys must be a TransferFunction \ -object.") + raise TypeError("tf2ss(sys): sys must be a TransferFunction " + "object.") return _convertToStateSpace(sys) else: raise ValueError("Needs 1 or 2 arguments; received %i." % len(args)) diff --git a/control/tests/convert_test.py b/control/tests/convert_test.py index de1cf01d1..f92029fe3 100644 --- a/control/tests/convert_test.py +++ b/control/tests/convert_test.py @@ -250,3 +250,16 @@ def test_tf2ss_robustness(self): np.testing.assert_array_almost_equal(np.sort(sys2tf.pole()), np.sort(sys2ss.pole())) + def test_tf2ss_nonproper(self): + """Unit tests for non-proper transfer functions""" + # Easy case: input 2 to output 1 is 's' + num = [ [[0], [1, 0]], [[1], [0]] ] + den1 = [ [[1], [1]], [[1,4], [1]] ] + with pytest.raises(ValueError): + tf2ss(tf(num, den1)) + + # Trickier case (make sure that leading zeros in den are handled) + num = [ [[0], [1, 0]], [[1], [0]] ] + den1 = [ [[1], [0, 1]], [[1,4], [1]] ] + with pytest.raises(ValueError): + tf2ss(tf(num, den1)) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index faed39e07..d96eefd39 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -18,7 +18,6 @@ from control import iosys as ios from control.tests.conftest import noscipy0 - class TestIOSys: @pytest.fixture @@ -81,6 +80,11 @@ def test_tf2io(self, tsys): np.testing.assert_array_almost_equal(lti_t, ios_t) np.testing.assert_allclose(lti_y, ios_y, atol=0.002, rtol=0.) + # Make sure that non-proper transfer functions generate an error + tfsys = ct.tf('s') + with pytest.raises(ValueError): + iosys=ct.tf2io(tfsys) + def test_ss2io(self, tsys): # Create an input/output system from the linear system linsys = tsys.siso_linsys 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