From 5a4c3ab18f8d0129dfd90d92f6a04eadcbbf5fb8 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 19 Jan 2021 20:17:19 -0800 Subject: [PATCH 1/4] convert inputs, outputs, ... to ninputs... w/ getter/setter warning --- control/bdalg.py | 16 +-- control/canonical.py | 14 +-- control/frdata.py | 54 ++++---- control/freqplot.py | 6 +- control/iosys.py | 22 ++-- control/lti.py | 49 +++++++- control/robust.py | 26 ++-- control/statefbk.py | 4 +- control/statesp.py | 217 ++++++++++++++++++--------------- control/tests/convert_test.py | 16 +-- control/tests/freqresp_test.py | 2 +- control/tests/iosys_test.py | 8 +- control/tests/lti_test.py | 16 +-- control/tests/matlab_test.py | 6 +- control/tests/minreal_test.py | 8 +- control/tests/robust_test.py | 32 ++--- control/tests/statesp_test.py | 30 ++--- control/tests/timeresp_test.py | 54 ++++---- control/tests/xferfcn_test.py | 46 +++---- control/timeresp.py | 16 +-- control/xferfcn.py | 188 ++++++++++++++-------------- 21 files changed, 445 insertions(+), 385 deletions(-) diff --git a/control/bdalg.py b/control/bdalg.py index e00dcfa3c..20c9f4b09 100644 --- a/control/bdalg.py +++ b/control/bdalg.py @@ -76,7 +76,7 @@ def series(sys1, *sysn): Raises ------ ValueError - if `sys2.inputs` does not equal `sys1.outputs` + if `sys2.ninputs` does not equal `sys1.noutputs` if `sys1.dt` is not compatible with `sys2.dt` See Also @@ -336,25 +336,25 @@ def connect(sys, Q, inputv, outputv): """ inputv, outputv, Q = np.asarray(inputv), np.asarray(outputv), np.asarray(Q) # check indices - index_errors = (inputv - 1 > sys.inputs) | (inputv < 1) + index_errors = (inputv - 1 > sys.ninputs) | (inputv < 1) if np.any(index_errors): raise IndexError( "inputv index %s out of bounds" % inputv[np.where(index_errors)]) - index_errors = (outputv - 1 > sys.outputs) | (outputv < 1) + index_errors = (outputv - 1 > sys.noutputs) | (outputv < 1) if np.any(index_errors): raise IndexError( "outputv index %s out of bounds" % outputv[np.where(index_errors)]) - index_errors = (Q[:,0:1] - 1 > sys.inputs) | (Q[:,0:1] < 1) + index_errors = (Q[:,0:1] - 1 > sys.ninputs) | (Q[:,0:1] < 1) if np.any(index_errors): raise IndexError( "Q input index %s out of bounds" % Q[np.where(index_errors)]) - index_errors = (np.abs(Q[:,1:]) - 1 > sys.outputs) + index_errors = (np.abs(Q[:,1:]) - 1 > sys.noutputs) if np.any(index_errors): raise IndexError( "Q output index %s out of bounds" % Q[np.where(index_errors)]) # first connect - K = np.zeros((sys.inputs, sys.outputs)) + K = np.zeros((sys.ninputs, sys.noutputs)) for r in np.array(Q).astype(int): inp = r[0]-1 for outp in r[1:]: @@ -365,8 +365,8 @@ def connect(sys, Q, inputv, outputv): sys = sys.feedback(np.array(K), sign=1) # now trim - Ytrim = np.zeros((len(outputv), sys.outputs)) - Utrim = np.zeros((sys.inputs, len(inputv))) + Ytrim = np.zeros((len(outputv), sys.noutputs)) + Utrim = np.zeros((sys.ninputs, len(inputv))) for i,u in enumerate(inputv): Utrim[u-1,i] = 1. for i,y in enumerate(outputv): diff --git a/control/canonical.py b/control/canonical.py index 341ec5da4..45846147f 100644 --- a/control/canonical.py +++ b/control/canonical.py @@ -79,16 +79,16 @@ def reachable_form(xsys): zsys.B[0, 0] = 1.0 zsys.A = zeros_like(xsys.A) Apoly = poly(xsys.A) # characteristic polynomial - for i in range(0, xsys.states): + for i in range(0, xsys.nstates): zsys.A[0, i] = -Apoly[i+1] / Apoly[0] - if (i+1 < xsys.states): + if (i+1 < xsys.nstates): zsys.A[i+1, i] = 1.0 # Compute the reachability matrices for each set of states Wrx = ctrb(xsys.A, xsys.B) Wrz = ctrb(zsys.A, zsys.B) - if matrix_rank(Wrx) != xsys.states: + if matrix_rank(Wrx) != xsys.nstates: raise ValueError("System not controllable to working precision.") # Transformation from one form to another @@ -96,7 +96,7 @@ def reachable_form(xsys): # Check to make sure inversion was OK. Note that since we are inverting # Wrx and we already checked its rank, this exception should never occur - if matrix_rank(Tzx) != xsys.states: # pragma: no cover + if matrix_rank(Tzx) != xsys.nstates: # pragma: no cover raise ValueError("Transformation matrix singular to working precision.") # Finally, compute the output matrix @@ -133,9 +133,9 @@ def observable_form(xsys): zsys.C[0, 0] = 1 zsys.A = zeros_like(xsys.A) Apoly = poly(xsys.A) # characteristic polynomial - for i in range(0, xsys.states): + for i in range(0, xsys.nstates): zsys.A[i, 0] = -Apoly[i+1] / Apoly[0] - if (i+1 < xsys.states): + if (i+1 < xsys.nstates): zsys.A[i, i+1] = 1 # Compute the observability matrices for each set of states @@ -145,7 +145,7 @@ def observable_form(xsys): # Transformation from one form to another Tzx = solve(Wrz, Wrx) # matrix left division, Tzx = inv(Wrz) * Wrx - if matrix_rank(Tzx) != xsys.states: + if matrix_rank(Tzx) != xsys.nstates: raise ValueError("Transformation matrix singular to working precision.") # Finally, compute the output matrix diff --git a/control/frdata.py b/control/frdata.py index 844ac9ab9..3398bfbee 100644 --- a/control/frdata.py +++ b/control/frdata.py @@ -155,11 +155,11 @@ def __init__(self, *args, **kwargs): def __str__(self): """String representation of the transfer function.""" - mimo = self.inputs > 1 or self.outputs > 1 + mimo = self.ninputs > 1 or self.noutputs > 1 outstr = ['Frequency response data'] - for i in range(self.inputs): - for j in range(self.outputs): + for i in range(self.ninputs): + for j in range(self.noutputs): if mimo: outstr.append("Input %i to output %i:" % (i + 1, j + 1)) outstr.append('Freq [rad/s] Response') @@ -201,12 +201,12 @@ def __add__(self, other): other = _convert_to_FRD(other, omega=self.omega) # Check that the input-output sizes are consistent. - if self.inputs != other.inputs: + if self.ninputs != other.ninputs: raise ValueError("The first summand has %i input(s), but the \ -second has %i." % (self.inputs, other.inputs)) - if self.outputs != other.outputs: +second has %i." % (self.ninputs, other.ninputs)) + if self.noutputs != other.noutputs: raise ValueError("The first summand has %i output(s), but the \ -second has %i." % (self.outputs, other.outputs)) +second has %i." % (self.noutputs, other.noutputs)) return FRD(self.fresp + other.fresp, other.omega) @@ -236,14 +236,14 @@ def __mul__(self, other): other = _convert_to_FRD(other, omega=self.omega) # Check that the input-output sizes are consistent. - if self.inputs != other.outputs: + if self.ninputs != other.noutputs: raise ValueError( "H = G1*G2: input-output size mismatch: " "G1 has %i input(s), G2 has %i output(s)." % - (self.inputs, other.outputs)) + (self.ninputs, other.noutputs)) - inputs = other.inputs - outputs = self.outputs + inputs = other.ninputs + outputs = self.noutputs fresp = empty((outputs, inputs, len(self.omega)), dtype=self.fresp.dtype) for i in range(len(self.omega)): @@ -263,14 +263,14 @@ def __rmul__(self, other): other = _convert_to_FRD(other, omega=self.omega) # Check that the input-output sizes are consistent. - if self.outputs != other.inputs: + if self.noutputs != other.ninputs: raise ValueError( "H = G1*G2: input-output size mismatch: " "G1 has %i input(s), G2 has %i output(s)." % - (other.inputs, self.outputs)) + (other.ninputs, self.noutputs)) - inputs = self.inputs - outputs = other.outputs + inputs = self.ninputs + outputs = other.noutputs fresp = empty((outputs, inputs, len(self.omega)), dtype=self.fresp.dtype) @@ -290,8 +290,8 @@ def __truediv__(self, other): else: other = _convert_to_FRD(other, omega=self.omega) - if (self.inputs > 1 or self.outputs > 1 or - other.inputs > 1 or other.outputs > 1): + if (self.ninputs > 1 or self.noutputs > 1 or + other.ninputs > 1 or other.noutputs > 1): raise NotImplementedError( "FRD.__truediv__ is currently only implemented for SISO " "systems.") @@ -313,8 +313,8 @@ def __rtruediv__(self, other): else: other = _convert_to_FRD(other, omega=self.omega) - if (self.inputs > 1 or self.outputs > 1 or - other.inputs > 1 or other.outputs > 1): + if (self.ninputs > 1 or self.noutputs > 1 or + other.ninputs > 1 or other.noutputs > 1): raise NotImplementedError( "FRD.__rtruediv__ is currently only implemented for " "SISO systems.") @@ -392,10 +392,10 @@ def eval(self, omega, squeeze=None): else: out = self.fresp[:, :, elements] else: - out = empty((self.outputs, self.inputs, len(omega_array)), + out = empty((self.noutputs, self.ninputs, len(omega_array)), dtype=complex) - for i in range(self.outputs): - for j in range(self.inputs): + for i in range(self.noutputs): + for j in range(self.ninputs): for k, w in enumerate(omega_array): frraw = splev(w, self.ifunc[i, j], der=0) out[i, j, k] = frraw[0] + 1.0j * frraw[1] @@ -406,7 +406,7 @@ def __call__(self, s, squeeze=None): """Evaluate system's transfer function at complex frequencies. Returns the complex frequency response `sys(s)` of system `sys` with - `m = sys.inputs` number of inputs and `p = sys.outputs` number of + `m = sys.ninputs` number of inputs and `p = sys.noutputs` number of outputs. To evaluate at a frequency omega in radians per second, enter @@ -474,10 +474,10 @@ def feedback(self, other=1, sign=-1): other = _convert_to_FRD(other, omega=self.omega) - if (self.outputs != other.inputs or self.inputs != other.outputs): + if (self.noutputs != other.ninputs or self.ninputs != other.noutputs): raise ValueError( "FRD.feedback, inputs/outputs mismatch") - fresp = empty((self.outputs, self.inputs, len(other.omega)), + fresp = empty((self.noutputs, self.ninputs, len(other.omega)), dtype=complex) # TODO: vectorize this # TODO: handle omega re-mapping @@ -487,9 +487,9 @@ def feedback(self, other=1, sign=-1): fresp[:, :, k] = np.dot( self.fresp[:, :, k], linalg.solve( - eye(self.inputs) + eye(self.ninputs) + np.dot(other.fresp[:, :, k], self.fresp[:, :, k]), - eye(self.inputs)) + eye(self.ninputs)) ) return FRD(fresp, other.omega, smooth=(self.ifunc is not None)) diff --git a/control/freqplot.py b/control/freqplot.py index 38b525aec..ef4263bbe 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -214,7 +214,7 @@ def bode_plot(syslist, omega=None, mags, phases, omegas, nyquistfrqs = [], [], [], [] for sys in syslist: - if sys.inputs > 1 or sys.outputs > 1: + if sys.ninputs > 1 or sys.noutputs > 1: # TODO: Add MIMO bode plots. raise NotImplementedError( "Bode is currently only implemented for SISO systems.") @@ -582,7 +582,7 @@ def nyquist_plot(syslist, omega=None, plot=True, label_freq=0, num=50, endpoint=True, base=10.0) for sys in syslist: - if sys.inputs > 1 or sys.outputs > 1: + if sys.ninputs > 1 or sys.noutputs > 1: # TODO: Add MIMO nyquist plots. raise NotImplementedError( "Nyquist is currently only implemented for SISO systems.") @@ -672,7 +672,7 @@ def gangof4_plot(P, C, omega=None, **kwargs): ------- None """ - if P.inputs > 1 or P.outputs > 1 or C.inputs > 1 or C.outputs > 1: + if P.ninputs > 1 or P.noutputs > 1 or C.ninputs > 1 or C.noutputs > 1: # TODO: Add MIMO go4 plots. raise NotImplementedError( "Gang of four is currently only implemented for SISO systems.") diff --git a/control/iosys.py b/control/iosys.py index 66ca20ebb..1021861b0 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -659,8 +659,8 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None, # Create the I/O system object super(LinearIOSystem, self).__init__( - inputs=linsys.inputs, outputs=linsys.outputs, - states=linsys.states, params={}, dt=linsys.dt, name=name) + inputs=linsys.ninputs, outputs=linsys.noutputs, + states=linsys.nstates, params={}, dt=linsys.dt, name=name) # Initalize additional state space variables StateSpace.__init__(self, linsys, remove_useless=False) @@ -668,16 +668,16 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None, # Process input, output, state lists, if given # Make sure they match the size of the linear system ninputs, self.input_index = self._process_signal_list( - inputs if inputs is not None else linsys.inputs, prefix='u') - if ninputs is not None and linsys.inputs != ninputs: + inputs if inputs is not None else linsys.ninputs, prefix='u') + if ninputs is not None and linsys.ninputs != ninputs: raise ValueError("Wrong number/type of inputs given.") noutputs, self.output_index = self._process_signal_list( - outputs if outputs is not None else linsys.outputs, prefix='y') - if noutputs is not None and linsys.outputs != noutputs: + outputs if outputs is not None else linsys.noutputs, prefix='y') + if noutputs is not None and linsys.noutputs != noutputs: raise ValueError("Wrong number/type of outputs given.") nstates, self.state_index = self._process_signal_list( - states if states is not None else linsys.states, prefix='x') - if nstates is not None and linsys.states != nstates: + states if states is not None else linsys.nstates, prefix='x') + if nstates is not None and linsys.nstates != nstates: raise ValueError("Wrong number/type of states given.") def _update_params(self, params={}, warning=True): @@ -1345,9 +1345,9 @@ def __init__(self, io_sys, ss_sys=None): # Initialize the state space attributes if isinstance(ss_sys, StateSpace): # Make sure the dimension match - if io_sys.ninputs != ss_sys.inputs or \ - io_sys.noutputs != ss_sys.outputs or \ - io_sys.nstates != ss_sys.states: + if io_sys.ninputs != ss_sys.ninputs or \ + io_sys.noutputs != ss_sys.noutputs or \ + io_sys.nstates != ss_sys.nstates: raise ValueError("System dimensions for first and second " "arguments must match.") StateSpace.__init__(self, ss_sys, remove_useless=False) diff --git a/control/lti.py b/control/lti.py index 514944f75..04f495838 100644 --- a/control/lti.py +++ b/control/lti.py @@ -47,10 +47,47 @@ def __init__(self, inputs=1, outputs=1, dt=None): """Assign the LTI object's numbers of inputs and ouputs.""" # Data members common to StateSpace and TransferFunction. - self.inputs = inputs - self.outputs = outputs + self.ninputs = inputs + self.noutputs = outputs self.dt = dt + # + # Getter and setter functions for legacy input/output attributes + # + # For this iteration, generate a warning whenever the getter/setter is + # called. For a future iteration, turn it iinto a pending deprecation and + # then deprecation warning (commented out for now). + # + + @property + def inputs(self): + raise PendingDeprecationWarning( + "The LTI `inputs` attribute will be deprecated in a future " + "release. Use `ninputs` instead.") + return self.ninputs + + @inputs.setter + def inputs(self, value): + raise PendingDeprecationWarning( + "The LTI `inputs` attribute will be deprecated in a future " + "release. Use `ninputs` instead.") + + self.ninputs = value + + @property + def outputs(self): + raise PendingDeprecationWarning( + "The LTI `outputs` attribute will be deprecated in a future " + "release. Use `noutputs` instead.") + return self.noutputs + + @outputs.setter + def outputs(self, value): + raise PendingDeprecationWarning( + "The LTI `outputs` attribute will be deprecated in a future " + "release. Use `noutputs` instead.") + self.noutputs = value + def isdtime(self, strict=False): """ Check to see if a system is a discrete-time system @@ -88,7 +125,7 @@ def isctime(self, strict=False): def issiso(self): '''Check to see if a system is single input, single output''' - return self.inputs == 1 and self.outputs == 1 + return self.ninputs == 1 and self.noutputs == 1 def damp(self): '''Natural frequency, damping ratio of system poles @@ -126,7 +163,7 @@ def frequency_response(self, omega, squeeze=None): G(exp(j*omega*dt)) = mag*exp(j*phase). In general the system may be multiple input, multiple output (MIMO), - where `m = self.inputs` number of inputs and `p = self.outputs` number + where `m = self.ninputs` number of inputs and `p = self.noutputs` number of outputs. Parameters @@ -475,7 +512,7 @@ def evalfr(sys, x, squeeze=None): Returns the complex frequency response `sys(x)` where `x` is `s` for continuous-time systems and `z` for discrete-time systems, with - `m = sys.inputs` number of inputs and `p = sys.outputs` number of + `m = sys.ninputs` number of inputs and `p = sys.noutputs` number of outputs. To evaluate at a frequency omega in radians per second, enter @@ -532,7 +569,7 @@ def freqresp(sys, omega, squeeze=None): """Frequency response of an LTI system at multiple angular frequencies. In general the system may be multiple input, multiple output (MIMO), where - `m = sys.inputs` number of inputs and `p = sys.outputs` number of + `m = sys.ninputs` number of inputs and `p = sys.noutputs` number of outputs. Parameters diff --git a/control/robust.py b/control/robust.py index 2584339ac..aa5c922dc 100644 --- a/control/robust.py +++ b/control/robust.py @@ -206,12 +206,12 @@ def _size_as_needed(w, wname, n): if w is not None: if not isinstance(w, StateSpace): w = ss(w) - if 1 == w.inputs and 1 == w.outputs: + if 1 == w.ninputs and 1 == w.noutputs: w = append(*(w,) * n) else: - if w.inputs != n: + if w.ninputs != n: msg = ("{}: weighting function has {} inputs, expected {}". - format(wname, w.inputs, n)) + format(wname, w.ninputs, n)) raise ValueError(msg) else: w = ss([], [], [], []) @@ -253,8 +253,8 @@ def augw(g, w1=None, w2=None, w3=None): if w1 is None and w2 is None and w3 is None: raise ValueError("At least one weighting must not be None") - ny = g.outputs - nu = g.inputs + ny = g.noutputs + nu = g.ninputs w1, w2, w3 = [_size_as_needed(w, wname, n) for w, wname, n in zip((w1, w2, w3), @@ -278,13 +278,13 @@ def augw(g, w1=None, w2=None, w3=None): sysall = append(w1, w2, w3, Ie, g, Iu) - niw1 = w1.inputs - niw2 = w2.inputs - niw3 = w3.inputs + niw1 = w1.ninputs + niw2 = w2.ninputs + niw3 = w3.ninputs - now1 = w1.outputs - now2 = w2.outputs - now3 = w3.outputs + now1 = w1.noutputs + now2 = w2.noutputs + now3 = w3.noutputs q = np.zeros((niw1 + niw2 + niw3 + ny + nu, 2)) q[:, 0] = np.arange(1, q.shape[0] + 1) @@ -358,8 +358,8 @@ def mixsyn(g, w1=None, w2=None, w3=None): -------- hinfsyn, augw """ - nmeas = g.outputs - ncon = g.inputs + nmeas = g.noutputs + ncon = g.ninputs p = augw(g, w1, w2, w3) k, cl, gamma, rcond = hinfsyn(p, nmeas, ncon) diff --git a/control/statefbk.py b/control/statefbk.py index 1d51cfc61..7106449ae 100644 --- a/control/statefbk.py +++ b/control/statefbk.py @@ -624,7 +624,7 @@ def gram(sys, type): elif type == 'o': tra = 'N' C = -np.dot(sys.C.transpose(), sys.C) - n = sys.states + n = sys.nstates U = np.zeros((n, n)) A = np.array(sys.A) # convert to NumPy array for slycot X, scale, sep, ferr, w = sb03md( @@ -639,7 +639,7 @@ def gram(sys, type): except ImportError: raise ControlSlycot("can't find slycot module 'sb03od'") tra = 'N' - n = sys.states + n = sys.nstates Q = np.zeros((n, n)) A = np.array(sys.A) # convert to NumPy array for slycot if type == 'cf': diff --git a/control/statesp.py b/control/statesp.py index 0ee83cb7d..a6851e531 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -305,31 +305,54 @@ def __init__(self, *args, **kwargs): else: dt = config.defaults['control.default_dt'] self.dt = dt - self.states = A.shape[1] + self.nstates = A.shape[1] - if 0 == self.states: + if 0 == self.nstates: # static gain # matrix's default "empty" shape is 1x0 A.shape = (0, 0) - B.shape = (0, self.inputs) - C.shape = (self.outputs, 0) + B.shape = (0, self.ninputs) + C.shape = (self.noutputs, 0) # Check that the matrix sizes are consistent. - if self.states != A.shape[0]: + if self.nstates != A.shape[0]: raise ValueError("A must be square.") - if self.states != B.shape[0]: + if self.nstates != B.shape[0]: raise ValueError("A and B must have the same number of rows.") - if self.states != C.shape[1]: + if self.nstates != C.shape[1]: raise ValueError("A and C must have the same number of columns.") - if self.inputs != B.shape[1]: + if self.ninputs != B.shape[1]: raise ValueError("B and D must have the same number of columns.") - if self.outputs != C.shape[0]: + if self.noutputs != C.shape[0]: 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_states: self._remove_useless_states() + # + # Getter and setter functions for legacy state attributes + # + # For this iteration, generate a warning whenever the getter/setter is + # called. For a future iteration, turn it iinto a pending deprecation and + # then deprecation warning (commented out for now). + # + + @property + def states(self): + raise PendingDeprecationWarning( + "The StateSpace `states` attribute will be deprecated in a future " + "release. Use `nstates` instead.") + return self.nstates + + @states.setter + def states(self, value): + raise PendingDeprecationWarning( + "The StateSpace `states` attribute will be deprecated in a future " + "release. Use `nstates` instead.") + # raise PendingDeprecationWarning( + self.nstates = value + def _remove_useless_states(self): """Check for states that don't do anything, and remove them. @@ -358,9 +381,9 @@ def _remove_useless_states(self): self.B = delete(self.B, useless, 0) self.C = delete(self.C, useless, 1) - self.states = self.A.shape[0] - self.inputs = self.B.shape[1] - self.outputs = self.C.shape[0] + self.nstates = self.A.shape[0] + self.ninputs = self.B.shape[1] + self.noutputs = self.C.shape[0] def __str__(self): """Return string representation of the state space system.""" @@ -398,7 +421,7 @@ def _latex_partitioned_stateless(self): r'\[', r'\left(', (r'\begin{array}' - + r'{' + 'rll' * self.inputs + '}') + + r'{' + 'rll' * self.ninputs + '}') ] for Di in asarray(self.D): @@ -422,14 +445,14 @@ def _latex_partitioned(self): ------- s : string with LaTeX representation of model """ - if self.states == 0: + if self.nstates == 0: return self._latex_partitioned_stateless() lines = [ r'\[', r'\left(', (r'\begin{array}' - + r'{' + 'rll' * self.states + '|' + 'rll' * self.inputs + '}') + + r'{' + 'rll' * self.nstates + '|' + 'rll' * self.ninputs + '}') ] for Ai, Bi in zip(asarray(self.A), asarray(self.B)): @@ -476,7 +499,7 @@ def fmt_matrix(matrix, name): r'\right)']) return matlines - if self.states > 0: + if self.nstates > 0: lines.extend(fmt_matrix(self.A, 'A')) lines.append('&') lines.extend(fmt_matrix(self.B, 'B')) @@ -536,8 +559,8 @@ def __add__(self, other): other = _convert_to_statespace(other) # Check to make sure the dimensions are OK - if ((self.inputs != other.inputs) or - (self.outputs != other.outputs)): + if ((self.ninputs != other.ninputs) or + (self.noutputs != other.noutputs)): raise ValueError("Systems have different shapes.") dt = common_timebase(self.dt, other.dt) @@ -586,9 +609,9 @@ def __mul__(self, other): other = _convert_to_statespace(other) # Check to make sure the dimensions are OK - if self.inputs != other.outputs: + if self.ninputs != other.noutputs: raise ValueError("C = A * B: A has %i column(s) (input(s)), \ - but B has %i row(s)\n(output(s))." % (self.inputs, other.outputs)) + but B has %i row(s)\n(output(s))." % (self.ninputs, other.noutputs)) dt = common_timebase(self.dt, other.dt) # Concatenate the various arrays @@ -708,9 +731,9 @@ def slycot_laub(self, x): raise ValueError("input list must be 1D") # preallocate - n = self.states - m = self.inputs - p = self.outputs + n = self.nstates + m = self.ninputs + p = self.noutputs out = np.empty((p, m, len(x_arr)), dtype=complex) # The first call both evaluates C(sI-A)^-1 B and also returns # Hessenberg transformed matrices at, bt, ct. @@ -753,7 +776,7 @@ def horner(self, x): Returns ------- - output : (self.outputs, self.inputs, len(x)) complex ndarray + output : (self.noutputs, self.ninputs, len(x)) complex ndarray Frequency response Notes @@ -773,13 +796,13 @@ def horner(self, x): raise ValueError("input list must be 1D") # Preallocate - out = empty((self.outputs, self.inputs, len(x_arr)), dtype=complex) + out = empty((self.noutputs, self.ninputs, len(x_arr)), dtype=complex) #TODO: can this be vectorized? for idx, x_idx in enumerate(x_arr): out[:,:,idx] = \ np.dot(self.C, - solve(x_idx * eye(self.states) - self.A, self.B)) \ + solve(x_idx * eye(self.nstates) - self.A, self.B)) \ + self.D return out @@ -801,12 +824,12 @@ def freqresp(self, omega): def pole(self): """Compute the poles of a state space system.""" - return eigvals(self.A) if self.states else np.array([]) + return eigvals(self.A) if self.nstates else np.array([]) def zero(self): """Compute the zeros of a state space system.""" - if not self.states: + if not self.nstates: return np.array([]) # Use AB08ND from Slycot if it's available, otherwise use @@ -854,7 +877,7 @@ def feedback(self, other=1, sign=-1): other = _convert_to_statespace(other) # Check to make sure the dimensions are OK - if (self.inputs != other.outputs) or (self.outputs != other.inputs): + if (self.ninputs != other.noutputs) or (self.noutputs != other.ninputs): raise ValueError("State space systems don't have compatible " "inputs/outputs for feedback.") dt = common_timebase(self.dt, other.dt) @@ -868,8 +891,8 @@ def feedback(self, other=1, sign=-1): C2 = other.C D2 = other.D - F = eye(self.inputs) - sign * np.dot(D2, D1) - if matrix_rank(F) != self.inputs: + F = eye(self.ninputs) - sign * np.dot(D2, D1) + if matrix_rank(F) != self.ninputs: raise ValueError( "I - sign * D2 * D1 is singular to working precision.") @@ -879,11 +902,11 @@ def feedback(self, other=1, sign=-1): # decomposition (cubic runtime complexity) of F only once! # The remaining back substitutions are only quadratic in runtime. E_D2_C2 = solve(F, concatenate((D2, C2), axis=1)) - E_D2 = E_D2_C2[:, :other.inputs] - E_C2 = E_D2_C2[:, other.inputs:] + E_D2 = E_D2_C2[:, :other.ninputs] + E_C2 = E_D2_C2[:, other.ninputs:] - T1 = eye(self.outputs) + sign * np.dot(D1, E_D2) - T2 = eye(self.inputs) + sign * np.dot(E_D2, D1) + T1 = eye(self.noutputs) + sign * np.dot(D1, E_D2) + T2 = eye(self.ninputs) + sign * np.dot(E_D2, D1) A = concatenate( (concatenate( @@ -922,9 +945,9 @@ def lft(self, other, nu=-1, ny=-1): other = _convert_to_statespace(other) # maximal values for nu, ny if ny == -1: - ny = min(other.inputs, self.outputs) + ny = min(other.ninputs, self.noutputs) if nu == -1: - nu = min(other.outputs, self.inputs) + nu = min(other.noutputs, self.ninputs) # dimension check # TODO @@ -932,14 +955,14 @@ def lft(self, other, nu=-1, ny=-1): # submatrices A = self.A - B1 = self.B[:, :self.inputs - nu] - B2 = self.B[:, self.inputs - nu:] - C1 = self.C[:self.outputs - ny, :] - C2 = self.C[self.outputs - ny:, :] - D11 = self.D[:self.outputs - ny, :self.inputs - nu] - D12 = self.D[:self.outputs - ny, self.inputs - nu:] - D21 = self.D[self.outputs - ny:, :self.inputs - nu] - D22 = self.D[self.outputs - ny:, self.inputs - nu:] + B1 = self.B[:, :self.ninputs - nu] + B2 = self.B[:, self.ninputs - nu:] + C1 = self.C[:self.noutputs - ny, :] + C2 = self.C[self.noutputs - ny:, :] + D11 = self.D[:self.noutputs - ny, :self.ninputs - nu] + D12 = self.D[:self.noutputs - ny, self.ninputs - nu:] + D21 = self.D[self.noutputs - ny:, :self.ninputs - nu] + D22 = self.D[self.noutputs - ny:, self.ninputs - nu:] # submatrices Abar = other.A @@ -960,21 +983,21 @@ 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.nstates)), + D21, np.zeros((ny, other.ninputs - ny))], + [np.zeros((nu, self.nstates)), Cbar1, + np.zeros((nu, self.ninputs - 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] - H12 = TH[:ny, 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:] + T11 = TH[:ny, :self.nstates] + T12 = TH[:ny, self.nstates: self.nstates + other.nstates] + T21 = TH[ny:, :self.nstates] + T22 = TH[ny:, self.nstates: self.nstates + other.nstates] + H11 = TH[:ny, self.nstates + other.nstates:self.nstates + + other.nstates + self.ninputs - nu] + H12 = TH[:ny, self.nstates + other.nstates + self.ninputs - nu:] + H21 = TH[ny:, self.nstates + other.nstates:self.nstates + + other.nstates + self.ninputs - nu] + H22 = TH[ny:, self.nstates + other.nstates + self.ninputs - nu:] Ares = np.block([ [A + B2.dot(T21), B2.dot(T22)], @@ -1000,17 +1023,17 @@ def lft(self, other, nu=-1, ny=-1): def minreal(self, tol=0.0): """Calculate a minimal realization, removes unobservable and uncontrollable states""" - if self.states: + if self.nstates: try: from slycot import tb01pd - B = empty((self.states, max(self.inputs, self.outputs))) - B[:, :self.inputs] = self.B - C = empty((max(self.outputs, self.inputs), self.states)) - C[:self.outputs, :] = self.C - A, B, C, nr = tb01pd(self.states, self.inputs, self.outputs, + B = empty((self.nstates, max(self.ninputs, self.noutputs))) + B[:, :self.ninputs] = self.B + C = empty((max(self.noutputs, self.ninputs), self.nstates)) + C[:self.noutputs, :] = self.C + A, B, C, nr = tb01pd(self.nstates, self.ninputs, self.noutputs, 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.ninputs], + C[:self.noutputs, :nr], self.D) except ImportError: raise TypeError("minreal requires slycot tb01pd") else: @@ -1055,10 +1078,10 @@ def returnScipySignalLTI(self, strict=True): kwdt = {} # Preallocate the output. - out = [[[] for _ in range(self.inputs)] for _ in range(self.outputs)] + out = [[[] for _ in range(self.ninputs)] for _ in range(self.noutputs)] - for i in range(self.outputs): - for j in range(self.inputs): + for i in range(self.noutputs): + for j in range(self.ninputs): out[i][j] = signalStateSpace(asarray(self.A), asarray(self.B[:, j:j + 1]), asarray(self.C[i:i + 1, :]), @@ -1077,21 +1100,21 @@ def append(self, other): self.dt = common_timebase(self.dt, other.dt) - n = self.states + other.states - m = self.inputs + other.inputs - p = self.outputs + other.outputs + n = self.nstates + other.nstates + m = self.ninputs + other.ninputs + p = self.noutputs + other.noutputs A = zeros((n, n)) B = zeros((n, m)) C = zeros((p, n)) D = zeros((p, m)) - A[:self.states, :self.states] = self.A - A[self.states:, self.states:] = other.A - B[:self.states, :self.inputs] = self.B - B[self.states:, self.inputs:] = other.B - C[:self.outputs, :self.states] = self.C - C[self.outputs:, self.states:] = other.C - D[:self.outputs, :self.inputs] = self.D - D[self.outputs:, self.inputs:] = other.D + A[:self.nstates, :self.nstates] = self.A + A[self.nstates:, self.nstates:] = other.A + B[:self.nstates, :self.ninputs] = self.B + B[self.nstates:, self.ninputs:] = other.B + C[:self.noutputs, :self.nstates] = self.C + C[self.noutputs:, self.nstates:] = other.C + D[:self.noutputs, :self.ninputs] = self.D + D[self.noutputs:, self.ninputs:] = other.D return StateSpace(A, B, C, D, self.dt) def __getitem__(self, indices): @@ -1188,7 +1211,7 @@ def dcgain(self): gain = np.squeeze(self.horner(1)) except LinAlgError: # eigenvalue at DC - gain = np.tile(np.nan, (self.outputs, self.inputs)) + gain = np.tile(np.nan, (self.noutputs, self.ninputs)) return np.squeeze(gain) def is_static_gain(self): @@ -1241,13 +1264,13 @@ def _convert_to_statespace(sys, **kw): num, den, denorder = sys.minreal()._common_den() # transfer function to state space conversion now should work! - ssout = td04ad('C', sys.inputs, sys.outputs, + ssout = td04ad('C', sys.ninputs, sys.noutputs, 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], + ssout[2][:states, :sys.ninputs], + ssout[3][:sys.noutputs, :states], ssout[4], sys.dt) except ImportError: # No Slycot. Scipy tf->ss can't handle MIMO, but static @@ -1257,13 +1280,13 @@ def _convert_to_statespace(sys, **kw): maxd = max(max(len(d) for d in drow) 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)): + D = empty((sys.noutputs, sys.ninputs), dtype=float) + for i, j in itertools.product(range(sys.noutputs), + range(sys.ninputs)): D[i, j] = sys.num[i][j][0] / sys.den[i][j][0] return StateSpace([], [], [], D, sys.dt) else: - if sys.inputs != 1 or sys.outputs != 1: + if sys.ninputs != 1 or sys.noutputs != 1: raise TypeError("No support for MIMO without slycot") # TODO: do we want to squeeze first and check dimenations? @@ -1446,18 +1469,18 @@ def _mimo2siso(sys, input, output, warn_conversion=False): if not (isinstance(input, int) and isinstance(output, int)): raise TypeError("Parameters ``input`` and ``output`` must both " "be integer numbers.") - if not (0 <= input < sys.inputs): + if not (0 <= input < sys.ninputs): raise ValueError("Selected input does not exist. " "Selected input: {sel}, " "number of system inputs: {ext}." - .format(sel=input, ext=sys.inputs)) - if not (0 <= output < sys.outputs): + .format(sel=input, ext=sys.ninputs)) + if not (0 <= output < sys.noutputs): raise ValueError("Selected output does not exist. " "Selected output: {sel}, " "number of system outputs: {ext}." - .format(sel=output, ext=sys.outputs)) + .format(sel=output, ext=sys.noutputs)) # Convert sys to SISO if necessary - if sys.inputs > 1 or sys.outputs > 1: + if sys.ninputs > 1 or sys.noutputs > 1: if warn_conversion: warn("Converting MIMO system to SISO system. " "Only input {i} and output {o} are used." @@ -1502,13 +1525,13 @@ def _mimo2simo(sys, input, warn_conversion=False): """ if not (isinstance(input, int)): raise TypeError("Parameter ``input`` be an integer number.") - if not (0 <= input < sys.inputs): + if not (0 <= input < sys.ninputs): raise ValueError("Selected input does not exist. " "Selected input: {sel}, " "number of system inputs: {ext}." - .format(sel=input, ext=sys.inputs)) + .format(sel=input, ext=sys.ninputs)) # Convert sys to SISO if necessary - if sys.inputs > 1: + if sys.ninputs > 1: if warn_conversion: warn("Converting MIMO system to SIMO system. " "Only input {i} is used." .format(i=input)) diff --git a/control/tests/convert_test.py b/control/tests/convert_test.py index f92029fe3..de9c340f0 100644 --- a/control/tests/convert_test.py +++ b/control/tests/convert_test.py @@ -85,9 +85,9 @@ def testConvert(self, fixedseed, states, inputs, outputs): self.printSys(tfTransformed, 4) # Check to see if the state space systems have same dim - if (ssOriginal.states != ssTransformed.states) and verbose: + if (ssOriginal.nstates != ssTransformed.nstates) and verbose: print("WARNING: state space dimension mismatch: %d versus %d" % - (ssOriginal.states, ssTransformed.states)) + (ssOriginal.nstates, ssTransformed.nstates)) # Now make sure the frequency responses match # Since bode() only handles SISO, go through each I/O pair @@ -181,9 +181,9 @@ def testConvertMIMO(self): def testTf2ssStaticSiso(self): """Regression: tf2ss for SISO static gain""" gsiso = tf2ss(tf(23, 46)) - assert 0 == gsiso.states - assert 1 == gsiso.inputs - assert 1 == gsiso.outputs + assert 0 == gsiso.nstates + assert 1 == gsiso.ninputs + assert 1 == gsiso.noutputs # in all cases ratios are exactly representable, so assert_array_equal # is fine np.testing.assert_array_equal([[0.5]], gsiso.D) @@ -194,9 +194,9 @@ def testTf2ssStaticMimo(self): gmimo = tf2ss(tf( [[ [23], [3], [5] ], [ [-1], [0.125], [101.3] ]], [[ [46], [0.1], [80] ], [ [2], [-0.1], [1] ]])) - assert 0 == gmimo.states - assert 3 == gmimo.inputs - assert 2 == gmimo.outputs + assert 0 == gmimo.nstates + assert 3 == gmimo.ninputs + assert 2 == gmimo.noutputs d = np.array([[0.5, 30, 0.0625], [-0.5, -1.25, 101.3]]) np.testing.assert_array_equal(d, gmimo.D) diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py index 8da10a372..5c7c2cd80 100644 --- a/control/tests/freqresp_test.py +++ b/control/tests/freqresp_test.py @@ -231,7 +231,7 @@ def test_discrete(dsystem_type): dsys.frequency_response(omega_bad) # Test bode plots (currently only implemented for SISO) - if (dsys.inputs == 1 and dsys.outputs == 1): + if (dsys.ninputs == 1 and dsys.noutputs == 1): # Generic call (frequency range calculated automatically) bode(dsys) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index acdf43422..9a15e83f4 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -863,7 +863,7 @@ def test_named_signals(self, tsys): ).reshape(-1,), inputs = ['u[0]', 'u[1]'], outputs = ['y[0]', 'y[1]'], - states = tsys.mimo_linsys1.states, + states = tsys.mimo_linsys1.nstates, name = 'sys1') sys2 = ios.LinearIOSystem(tsys.mimo_linsys2, inputs = ['u[0]', 'u[1]'], @@ -974,7 +974,7 @@ def test_sys_naming_convention(self, tsys): outfcn=lambda t, x, u, params: u, inputs=('u[0]', 'u[1]'), outputs=('y[0]', 'y[1]'), - states=tsys.mimo_linsys1.states, + states=tsys.mimo_linsys1.nstates, name='namedsys') unnamedsys1 = ct.NonlinearIOSystem( lambda t, x, u, params: x, inputs=2, outputs=2, states=2 @@ -1107,7 +1107,7 @@ def outfcn(t, x, u, params): outfcn=outfcn, inputs=inputs, outputs=outputs, - states=tsys.mimo_linsys1.states, + states=tsys.mimo_linsys1.nstates, name='sys1') with pytest.raises(ValueError): sys1.linearize([0, 0], [0, 0]) @@ -1116,7 +1116,7 @@ def outfcn(t, x, u, params): outfcn=outfcn, inputs=('u[0]', 'u[1]'), outputs=('y[0]', 'y[1]'), - states=tsys.mimo_linsys1.states, + states=tsys.mimo_linsys1.nstates, name='sys1') for x0, u0 in [([0], [0, 0]), ([0, 0, 0], [0, 0]), diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index e165f9c60..b4a168841 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -222,17 +222,17 @@ def test_squeeze(self, fcn, nstate, nout, ninp, omega, squeeze, shape): ct.config.set_defaults('control', squeeze_frequency_response=False) mag, phase, _ = sys.frequency_response(omega) if isscalar: - assert mag.shape == (sys.outputs, sys.inputs, 1) - assert phase.shape == (sys.outputs, sys.inputs, 1) - assert sys(omega * 1j).shape == (sys.outputs, sys.inputs) - assert ct.evalfr(sys, omega * 1j).shape == (sys.outputs, sys.inputs) + assert mag.shape == (sys.noutputs, sys.ninputs, 1) + assert phase.shape == (sys.noutputs, sys.ninputs, 1) + assert sys(omega * 1j).shape == (sys.noutputs, sys.ninputs) + assert ct.evalfr(sys, omega * 1j).shape == (sys.noutputs, sys.ninputs) else: - assert mag.shape == (sys.outputs, sys.inputs, len(omega)) - assert phase.shape == (sys.outputs, sys.inputs, len(omega)) + assert mag.shape == (sys.noutputs, sys.ninputs, len(omega)) + assert phase.shape == (sys.noutputs, sys.ninputs, len(omega)) assert sys(omega * 1j).shape == \ - (sys.outputs, sys.inputs, len(omega)) + (sys.noutputs, sys.ninputs, len(omega)) assert ct.evalfr(sys, omega * 1j).shape == \ - (sys.outputs, sys.inputs, len(omega)) + (sys.noutputs, sys.ninputs, len(omega)) @pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.frd, ct.ss2io]) def test_squeeze_exceptions(self, fcn): diff --git a/control/tests/matlab_test.py b/control/tests/matlab_test.py index c9ba818cb..dd595be0f 100644 --- a/control/tests/matlab_test.py +++ b/control/tests/matlab_test.py @@ -697,9 +697,9 @@ def testMinreal(self, verbose=False): sys = ss(A, B, C, D) sysr = minreal(sys, verbose=verbose) - assert sysr.states == 2 - assert sysr.inputs == sys.inputs - assert sysr.outputs == sys.outputs + assert sysr.nstates == 2 + assert sysr.ninputs == sys.ninputs + assert sysr.noutputs == sys.noutputs np.testing.assert_array_almost_equal( eigvals(sysr.A), [-2.136154, -0.1638459]) diff --git a/control/tests/minreal_test.py b/control/tests/minreal_test.py index b2d166d5a..466f9384d 100644 --- a/control/tests/minreal_test.py +++ b/control/tests/minreal_test.py @@ -46,7 +46,7 @@ def testMinrealBrute(self): for n, m, p in permutations(range(1,6), 3): s = rss(n, p, m) sr = s.minreal() - if s.states > sr.states: + if s.nstates > sr.nstates: nreductions += 1 else: # Check to make sure that poles and zeros match @@ -98,9 +98,9 @@ def testMinrealSS(self): sys = StateSpace(A, B, C, D) sysr = sys.minreal() - assert sysr.states == 2 - assert sysr.inputs == sys.inputs - assert sysr.outputs == sys.outputs + assert sysr.nstates == 2 + assert sysr.ninputs == sys.ninputs + assert sysr.noutputs == sys.noutputs np.testing.assert_array_almost_equal( eigvals(sysr.A), [-2.136154, -0.1638459]) diff --git a/control/tests/robust_test.py b/control/tests/robust_test.py index 2c1a03ef6..146ae9e41 100644 --- a/control/tests/robust_test.py +++ b/control/tests/robust_test.py @@ -76,8 +76,8 @@ def testSisoW1(self): g = ss([-1.], [1.], [1.], [1.]) w1 = ss([-2], [2.], [1.], [2.]) p = augw(g, w1) - assert p.outputs == 2 - assert p.inputs == 2 + assert p.noutputs == 2 + assert p.ninputs == 2 # w->z1 should be w1 self.siso_almost_equal(w1, p[0, 0]) # w->v should be 1 @@ -93,8 +93,8 @@ def testSisoW2(self): g = ss([-1.], [1.], [1.], [1.]) w2 = ss([-2], [1.], [1.], [2.]) p = augw(g, w2=w2) - assert p.outputs == 2 - assert p.inputs == 2 + assert p.noutputs == 2 + assert p.ninputs == 2 # w->z2 should be 0 self.siso_almost_equal(ss([], [], [], 0), p[0, 0]) # w->v should be 1 @@ -110,8 +110,8 @@ def testSisoW3(self): g = ss([-1.], [1.], [1.], [1.]) w3 = ss([-2], [1.], [1.], [2.]) p = augw(g, w3=w3) - assert p.outputs == 2 - assert p.inputs == 2 + assert p.noutputs == 2 + assert p.ninputs == 2 # w->z3 should be 0 self.siso_almost_equal(ss([], [], [], 0), p[0, 0]) # w->v should be 1 @@ -129,8 +129,8 @@ def testSisoW123(self): w2 = ss([-3.], [3.], [1.], [3.]) w3 = ss([-4.], [4.], [1.], [4.]) p = augw(g, w1, w2, w3) - assert p.outputs == 4 - assert p.inputs == 2 + assert p.noutputs == 4 + assert p.ninputs == 2 # w->z1 should be w1 self.siso_almost_equal(w1, p[0, 0]) # w->z2 should be 0 @@ -157,8 +157,8 @@ def testMimoW1(self): [[1., 0.], [0., 1.]]) w1 = ss([-2], [2.], [1.], [2.]) p = augw(g, w1) - assert p.outputs == 4 - assert p.inputs == 4 + assert p.noutputs == 4 + assert p.ninputs == 4 # w->z1 should be diag(w1,w1) self.siso_almost_equal(w1, p[0, 0]) self.siso_almost_equal(0, p[0, 1]) @@ -189,8 +189,8 @@ def testMimoW2(self): [[1., 0.], [0., 1.]]) w2 = ss([-2], [2.], [1.], [2.]) p = augw(g, w2=w2) - assert p.outputs == 4 - assert p.inputs == 4 + assert p.noutputs == 4 + assert p.ninputs == 4 # w->z2 should be 0 self.siso_almost_equal(0, p[0, 0]) self.siso_almost_equal(0, p[0, 1]) @@ -221,8 +221,8 @@ def testMimoW3(self): [[1., 0.], [0., 1.]]) w3 = ss([-2], [2.], [1.], [2.]) p = augw(g, w3=w3) - assert p.outputs == 4 - assert p.inputs == 4 + assert p.noutputs == 4 + assert p.ninputs == 4 # w->z3 should be 0 self.siso_almost_equal(0, p[0, 0]) self.siso_almost_equal(0, p[0, 1]) @@ -261,8 +261,8 @@ def testMimoW123(self): [[11., 13.], [17., 19.]], [[23., 29.], [31., 37.]]) p = augw(g, w1, w2, w3) - assert p.outputs == 8 - assert p.inputs == 4 + assert p.noutputs == 8 + assert p.ninputs == 4 # w->z1 should be w1 self.siso_almost_equal(w1, p[0, 0]) self.siso_almost_equal(0, p[0, 1]) diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py index ccbd06881..9030b850a 100644 --- a/control/tests/statesp_test.py +++ b/control/tests/statesp_test.py @@ -417,9 +417,9 @@ def test_minreal(self): sys = StateSpace(A, B, C, D) sysr = sys.minreal() - assert sysr.states == 2 - assert sysr.inputs == sys.inputs - assert sysr.outputs == sys.outputs + assert sysr.nstates == 2 + assert sysr.ninputs == sys.ninputs + assert sysr.noutputs == sys.noutputs np.testing.assert_array_almost_equal( eigvals(sysr.A), [-2.136154, -0.1638459]) @@ -591,7 +591,7 @@ def test_remove_useless_states(self): assert (0, 4) == g1.B.shape assert (5, 0) == g1.C.shape assert (5, 4) == g1.D.shape - assert 0 == g1.states + assert 0 == g1.nstates @pytest.mark.parametrize("A, B, C, D", [([1], [], [], [1]), @@ -619,9 +619,9 @@ def test_minreal_static_gain(self): def test_empty(self): """Regression: can we create an empty StateSpace object?""" g1 = StateSpace([], [], [], []) - assert 0 == g1.states - assert 0 == g1.inputs - assert 0 == g1.outputs + assert 0 == g1.nstates + assert 0 == g1.ninputs + assert 0 == g1.noutputs def test_matrix_to_state_space(self): """_convert_to_statespace(matrix) gives ss([],[],[],D)""" @@ -758,9 +758,9 @@ class TestRss: def test_shape(self, states, outputs, inputs): """Test that rss outputs have the right state, input, and output size.""" sys = rss(states, outputs, inputs) - assert sys.states == states - assert sys.inputs == inputs - assert sys.outputs == outputs + assert sys.nstates == states + assert sys.ninputs == inputs + assert sys.noutputs == outputs @pytest.mark.parametrize('states', range(1, maxStates)) @pytest.mark.parametrize('outputs', range(1, maxIO)) @@ -787,9 +787,9 @@ class TestDrss: def test_shape(self, states, outputs, inputs): """Test that drss outputs have the right state, input, and output size.""" sys = drss(states, outputs, inputs) - assert sys.states == states - assert sys.inputs == inputs - assert sys.outputs == outputs + assert sys.nstates == states + assert sys.ninputs == inputs + assert sys.noutputs == outputs @pytest.mark.parametrize('states', range(1, maxStates)) @pytest.mark.parametrize('outputs', range(1, maxIO)) @@ -830,8 +830,8 @@ def mimoss(self, request): def test_returnScipySignalLTI(self, mimoss): """Test returnScipySignalLTI method with strict=False""" sslti = mimoss.returnScipySignalLTI(strict=False) - for i in range(mimoss.outputs): - for j in range(mimoss.inputs): + for i in range(mimoss.noutputs): + for j in range(mimoss.ninputs): np.testing.assert_allclose(sslti[i][j].A, mimoss.A) np.testing.assert_allclose(sslti[i][j].B, mimoss.B[:, j:j + 1]) diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py index 4bf52b498..f6c15f691 100644 --- a/control/tests/timeresp_test.py +++ b/control/tests/timeresp_test.py @@ -622,12 +622,12 @@ def test_time_vector(self, tsystem, fun, squeeze, matarrayout): t = tsystem.t kw['T'] = t if fun == forced_response: - kw['U'] = np.vstack([np.sin(t) for i in range(sys.inputs)]) + kw['U'] = np.vstack([np.sin(t) for i in range(sys.ninputs)]) elif fun == forced_response and isctime(sys): pytest.skip("No continuous forced_response without time vector.") - if hasattr(tsystem.sys, "states"): - kw['X0'] = np.arange(sys.states) + 1 - if sys.inputs > 1 and fun in [step_response, impulse_response]: + if hasattr(tsystem.sys, "nstates"): + kw['X0'] = np.arange(sys.nstates) + 1 + if sys.ninputs > 1 and fun in [step_response, impulse_response]: kw['input'] = 1 if squeeze is not None: kw['squeeze'] = squeeze @@ -641,7 +641,7 @@ def test_time_vector(self, tsystem, fun, squeeze, matarrayout): np.testing.assert_allclose(tout, tsystem.t) if squeeze is False or not sys.issiso(): - assert yout.shape[0] == sys.outputs + assert yout.shape[0] == sys.noutputs assert yout.shape[-1] == tout.shape[0] else: assert yout.shape == tout.shape @@ -668,8 +668,8 @@ def test_time_vector_interpolation(self, siso_dtf2, squeeze): tout, yout = forced_response(sys, t, u, x0, interpolate=True, **squeezekw) - if squeeze is False or sys.outputs > 1: - assert yout.shape[0] == sys.outputs + if squeeze is False or sys.noutputs > 1: + assert yout.shape[0] == sys.noutputs assert yout.shape[1] == tout.shape[0] else: assert yout.shape == tout.shape @@ -755,7 +755,7 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2): # Generate the time and input vectors tvec = np.linspace(0, 1, 8) uvec = np.dot( - np.ones((sys.inputs, 1)), + np.ones((sys.ninputs, 1)), np.reshape(np.sin(tvec), (1, 8))) # @@ -771,9 +771,9 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2): _, yvec, xvec = ct.impulse_response( sys, tvec, squeeze=squeeze, return_x=True) if sys.issiso(): - assert xvec.shape == (sys.states, 8) + assert xvec.shape == (sys.nstates, 8) else: - assert xvec.shape == (sys.states, sys.inputs, 8) + assert xvec.shape == (sys.nstates, sys.ninputs, 8) else: _, yvec = ct.impulse_response(sys, tvec, squeeze=squeeze) assert yvec.shape == shape1 @@ -784,9 +784,9 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2): _, yvec, xvec = ct.step_response( sys, tvec, squeeze=squeeze, return_x=True) if sys.issiso(): - assert xvec.shape == (sys.states, 8) + assert xvec.shape == (sys.nstates, 8) else: - assert xvec.shape == (sys.states, sys.inputs, 8) + assert xvec.shape == (sys.nstates, sys.ninputs, 8) else: _, yvec = ct.step_response(sys, tvec, squeeze=squeeze) assert yvec.shape == shape1 @@ -796,7 +796,7 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2): # Check the states as well _, yvec, xvec = ct.initial_response( sys, tvec, 1, squeeze=squeeze, return_x=True) - assert xvec.shape == (sys.states, 8) + assert xvec.shape == (sys.nstates, 8) else: _, yvec = ct.initial_response(sys, tvec, 1, squeeze=squeeze) assert yvec.shape == shape2 @@ -806,7 +806,7 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2): # Check the states as well _, yvec, xvec = ct.forced_response( sys, tvec, uvec, 0, return_x=True, squeeze=squeeze) - assert xvec.shape == (sys.states, 8) + assert xvec.shape == (sys.nstates, 8) else: # Just check the input/output response _, yvec = ct.forced_response(sys, tvec, uvec, 0, squeeze=squeeze) @@ -833,31 +833,31 @@ def test_squeeze(self, fcn, nstate, nout, ninp, squeeze, shape1, shape2): ct.config.set_defaults('control', squeeze_time_response=False) _, yvec = ct.impulse_response(sys, tvec) - if squeeze is not True or sys.inputs > 1 or sys.outputs > 1: - assert yvec.shape == (sys.outputs, sys.inputs, 8) + if squeeze is not True or sys.ninputs > 1 or sys.noutputs > 1: + assert yvec.shape == (sys.noutputs, sys.ninputs, 8) _, yvec = ct.step_response(sys, tvec) - if squeeze is not True or sys.inputs > 1 or sys.outputs > 1: - assert yvec.shape == (sys.outputs, sys.inputs, 8) + if squeeze is not True or sys.ninputs > 1 or sys.noutputs > 1: + assert yvec.shape == (sys.noutputs, sys.ninputs, 8) _, yvec = ct.initial_response(sys, tvec, 1) - if squeeze is not True or sys.outputs > 1: - assert yvec.shape == (sys.outputs, 8) + if squeeze is not True or sys.noutputs > 1: + assert yvec.shape == (sys.noutputs, 8) if isinstance(sys, ct.StateSpace): _, yvec, xvec = ct.forced_response( sys, tvec, uvec, 0, return_x=True) - assert xvec.shape == (sys.states, 8) + assert xvec.shape == (sys.nstates, 8) else: _, yvec = ct.forced_response(sys, tvec, uvec, 0) - if squeeze is not True or sys.outputs > 1: - assert yvec.shape == (sys.outputs, 8) + if squeeze is not True or sys.noutputs > 1: + assert yvec.shape == (sys.noutputs, 8) # For InputOutputSystems, also test input_output_response if isinstance(sys, ct.InputOutputSystem) and not scipy0: _, yvec = ct.input_output_response(sys, tvec, uvec) - if squeeze is not True or sys.outputs > 1: - assert yvec.shape == (sys.outputs, 8) + if squeeze is not True or sys.noutputs > 1: + assert yvec.shape == (sys.noutputs, 8) @pytest.mark.parametrize("fcn", [ct.ss, ct.tf, ct.ss2io]) def test_squeeze_exception(self, fcn): @@ -889,7 +889,7 @@ def test_squeeze_0_8_4(self, nstate, nout, ninp, squeeze, shape): sys = ct.rss(nstate, nout, ninp, strictly_proper=True) tvec = np.linspace(0, 1, 8) uvec = np.dot( - np.ones((sys.inputs, 1)), + np.ones((sys.ninputs, 1)), np.reshape(np.sin(tvec), (1, 8))) _, yvec = ct.initial_response(sys, tvec, 1, squeeze=squeeze) @@ -927,4 +927,4 @@ def test_response_transpose( sys, T, 1, transpose=True, return_x=True, squeeze=squeeze) assert t.shape == (T.size, ) assert y.shape == ysh_no - assert x.shape == (T.size, sys.states) + assert x.shape == (T.size, sys.nstates) diff --git a/control/tests/xferfcn_test.py b/control/tests/xferfcn_test.py index 4fc88c42e..73498ea44 100644 --- a/control/tests/xferfcn_test.py +++ b/control/tests/xferfcn_test.py @@ -187,8 +187,8 @@ def test_reverse_sign_mimo(self): sys2 = - sys1 sys3 = TransferFunction(num3, den1) - for i in range(sys3.outputs): - for j in range(sys3.inputs): + for i in range(sys3.noutputs): + for j in range(sys3.ninputs): np.testing.assert_array_equal(sys2.num[i][j], sys3.num[i][j]) np.testing.assert_array_equal(sys2.den[i][j], sys3.den[i][j]) @@ -233,8 +233,8 @@ def test_add_mimo(self): sys2 = TransferFunction(num2, den2) sys3 = sys1 + sys2 - for i in range(sys3.outputs): - for j in range(sys3.inputs): + for i in range(sys3.noutputs): + for j in range(sys3.ninputs): np.testing.assert_array_equal(sys3.num[i][j], num3[i][j]) np.testing.assert_array_equal(sys3.den[i][j], den3[i][j]) @@ -281,8 +281,8 @@ def test_subtract_mimo(self): sys2 = TransferFunction(num2, den2) sys3 = sys1 - sys2 - for i in range(sys3.outputs): - for j in range(sys3.inputs): + for i in range(sys3.noutputs): + for j in range(sys3.ninputs): np.testing.assert_array_equal(sys3.num[i][j], num3[i][j]) np.testing.assert_array_equal(sys3.den[i][j], den3[i][j]) @@ -337,8 +337,8 @@ def test_multiply_mimo(self): sys2 = TransferFunction(num2, den2) sys3 = sys1 * sys2 - for i in range(sys3.outputs): - for j in range(sys3.inputs): + for i in range(sys3.noutputs): + for j in range(sys3.ninputs): np.testing.assert_array_equal(sys3.num[i][j], num3[i][j]) np.testing.assert_array_equal(sys3.den[i][j], den3[i][j]) @@ -394,16 +394,16 @@ def test_slice(self): [ [ [1], [2], [3]], [ [3], [4], [5]] ], [ [[1, 2], [1, 3], [1, 4]], [[1, 4], [1, 5], [1, 6]] ]) sys1 = sys[1:, 1:] - assert (sys1.inputs, sys1.outputs) == (2, 1) + assert (sys1.ninputs, sys1.noutputs) == (2, 1) sys2 = sys[:2, :2] - assert (sys2.inputs, sys2.outputs) == (2, 2) + assert (sys2.ninputs, sys2.noutputs) == (2, 2) sys = TransferFunction( [ [ [1], [2], [3]], [ [3], [4], [5]] ], [ [[1, 2], [1, 3], [1, 4]], [[1, 4], [1, 5], [1, 6]] ], 0.5) sys1 = sys[1:, 1:] - assert (sys1.inputs, sys1.outputs) == (2, 1) + assert (sys1.ninputs, sys1.noutputs) == (2, 1) assert sys1.dt == 0.5 def test_is_static_gain(self): @@ -645,11 +645,11 @@ def test_convert_to_transfer_function(self): num = [[np.array([1., -7., 10.]), np.array([-1., 10.])], [np.array([2., -8.]), np.array([1., -2., -8.])], [np.array([1., 1., -30.]), np.array([7., -22.])]] - den = [[np.array([1., -5., -2.]) for _ in range(sys.inputs)] - for _ in range(sys.outputs)] + den = [[np.array([1., -5., -2.]) for _ in range(sys.ninputs)] + for _ in range(sys.noutputs)] - for i in range(sys.outputs): - for j in range(sys.inputs): + for i in range(sys.noutputs): + for j in range(sys.ninputs): np.testing.assert_array_almost_equal(tfsys.num[i][j], num[i][j]) np.testing.assert_array_almost_equal(tfsys.den[i][j], @@ -770,10 +770,10 @@ def test_matrix_array_multiply(self, matarrayin, X_, ij): # XH = X @ H XH = np.matmul(X, H) XH = XH.minreal() - assert XH.inputs == n - assert XH.outputs == X.shape[0] - assert len(XH.num) == XH.outputs - assert len(XH.den) == XH.outputs + assert XH.ninputs == n + assert XH.noutputs == X.shape[0] + assert len(XH.num) == XH.noutputs + assert len(XH.den) == XH.noutputs assert len(XH.num[0]) == n assert len(XH.den[0]) == n np.testing.assert_allclose(2. * H.num[ij][0], XH.num[0][0], rtol=1e-4) @@ -787,12 +787,12 @@ def test_matrix_array_multiply(self, matarrayin, X_, ij): # HXt = H @ X.T HXt = np.matmul(H, X.T) HXt = HXt.minreal() - assert HXt.inputs == X.T.shape[1] - assert HXt.outputs == n + assert HXt.ninputs == X.T.shape[1] + assert HXt.noutputs == n assert len(HXt.num) == n assert len(HXt.den) == n - assert len(HXt.num[0]) == HXt.inputs - assert len(HXt.den[0]) == HXt.inputs + assert len(HXt.num[0]) == HXt.ninputs + assert len(HXt.den[0]) == HXt.ninputs np.testing.assert_allclose(2. * H.num[0][ij], HXt.num[0][0], rtol=1e-4) np.testing.assert_allclose( H.den[0][ij], HXt.den[0][0], rtol=1e-4) np.testing.assert_allclose(2. * H.num[1][ij], HXt.num[1][0], rtol=1e-4) diff --git a/control/timeresp.py b/control/timeresp.py index bfa2ec149..4be0afbfd 100644 --- a/control/timeresp.py +++ b/control/timeresp.py @@ -709,13 +709,13 @@ def step_response(sys, T=None, X0=0., input=None, output=None, T_num=None, sys = _convert_to_statespace(sys) # Set up arrays to handle the output - ninputs = sys.inputs if input is None else 1 - noutputs = sys.outputs if output is None else 1 + ninputs = sys.ninputs if input is None else 1 + noutputs = sys.noutputs if output is None else 1 yout = np.empty((noutputs, ninputs, np.asarray(T).size)) - xout = np.empty((sys.states, ninputs, np.asarray(T).size)) + xout = np.empty((sys.nstates, ninputs, np.asarray(T).size)) # Simulate the response for each input - for i in range(sys.inputs): + for i in range(sys.ninputs): # If input keyword was specified, only simulate for that input if isinstance(input, int) and i != input: continue @@ -1025,13 +1025,13 @@ def impulse_response(sys, T=None, X0=0., input=None, output=None, T_num=None, U = np.zeros_like(T) # Set up arrays to handle the output - ninputs = sys.inputs if input is None else 1 - noutputs = sys.outputs if output is None else 1 + ninputs = sys.ninputs if input is None else 1 + noutputs = sys.noutputs if output is None else 1 yout = np.empty((noutputs, ninputs, np.asarray(T).size)) - xout = np.empty((sys.states, ninputs, np.asarray(T).size)) + xout = np.empty((sys.nstates, ninputs, np.asarray(T).size)) # Simulate the response for each input - for i in range(sys.inputs): + for i in range(sys.ninputs): # If input keyword was specified, only handle that case if isinstance(input, int) and i != input: continue diff --git a/control/xferfcn.py b/control/xferfcn.py index b732bafc0..5efee302f 100644 --- a/control/xferfcn.py +++ b/control/xferfcn.py @@ -241,8 +241,8 @@ def __call__(self, x, squeeze=None): continuous-time systems and `z` for discrete-time systems. In general the system may be multiple input, multiple output - (MIMO), where `m = self.inputs` number of inputs and `p = - self.outputs` number of outputs. + (MIMO), where `m = self.ninputs` number of inputs and `p = + self.noutputs` number of outputs. To evaluate at a frequency omega in radians per second, enter ``x = omega * 1j``, for continuous-time systems, or @@ -294,7 +294,7 @@ def horner(self, x): Returns ------- - output : (self.outputs, self.inputs, len(x)) complex ndarray + output : (self.noutputs, self.ninputs, len(x)) complex ndarray Frequency response """ @@ -304,9 +304,9 @@ def horner(self, x): if len(x_arr.shape) > 1: raise ValueError("input list must be 1D") - out = empty((self.outputs, self.inputs, len(x_arr)), dtype=complex) - for i in range(self.outputs): - for j in range(self.inputs): + out = empty((self.noutputs, self.ninputs, len(x_arr)), dtype=complex) + for i in range(self.noutputs): + for j in range(self.ninputs): out[i][j] = (polyval(self.num[i][j], x) / polyval(self.den[i][j], x)) return out @@ -323,8 +323,8 @@ def _truncatecoeff(self): # Beware: this is a shallow copy. This should be okay. data = [self.num, self.den] for p in range(len(data)): - for i in range(self.outputs): - for j in range(self.inputs): + for i in range(self.noutputs): + for j in range(self.ninputs): # Find the first nontrivial coefficient. nonzero = None for k in range(data[p][i][j].size): @@ -343,14 +343,14 @@ def _truncatecoeff(self): def __str__(self, var=None): """String representation of the transfer function.""" - mimo = self.inputs > 1 or self.outputs > 1 + mimo = self.ninputs > 1 or self.noutputs > 1 if var is None: # TODO: replace with standard calls to lti functions var = 's' if self.dt is None or self.dt == 0 else 'z' outstr = "" - for i in range(self.inputs): - for j in range(self.outputs): + for i in range(self.ninputs): + for j in range(self.noutputs): if mimo: outstr += "\nInput %i to output %i:" % (i + 1, j + 1) @@ -392,7 +392,7 @@ def __repr__(self): def _repr_latex_(self, var=None): """LaTeX representation of transfer function, for Jupyter notebook""" - mimo = self.inputs > 1 or self.outputs > 1 + mimo = self.ninputs > 1 or self.noutputs > 1 if var is None: # ! TODO: replace with standard calls to lti functions @@ -403,8 +403,8 @@ def _repr_latex_(self, var=None): if mimo: out.append(r"\begin{bmatrix}") - for i in range(self.outputs): - for j in range(self.inputs): + for i in range(self.noutputs): + for j in range(self.ninputs): # Convert the numerator and denominator polynomials to strings. numstr = _tf_polynomial_to_string(self.num[i][j], var=var) denstr = _tf_polynomial_to_string(self.den[i][j], var=var) @@ -414,7 +414,7 @@ def _repr_latex_(self, var=None): out += [r"\frac{", numstr, "}{", denstr, "}"] - if mimo and j < self.outputs - 1: + if mimo and j < self.noutputs - 1: out.append("&") if mimo: @@ -435,8 +435,8 @@ def __neg__(self): """Negate a transfer function.""" num = deepcopy(self.num) - for i in range(self.outputs): - for j in range(self.inputs): + for i in range(self.noutputs): + for j in range(self.ninputs): num[i][j] *= -1 return TransferFunction(num, self.den, self.dt) @@ -449,27 +449,27 @@ def __add__(self, other): if isinstance(other, StateSpace): other = _convert_to_transfer_function(other) elif not isinstance(other, TransferFunction): - other = _convert_to_transfer_function(other, inputs=self.inputs, - outputs=self.outputs) + other = _convert_to_transfer_function(other, inputs=self.ninputs, + outputs=self.noutputs) # Check that the input-output sizes are consistent. - if self.inputs != other.inputs: + if self.ninputs != other.ninputs: raise ValueError( "The first summand has %i input(s), but the second has %i." - % (self.inputs, other.inputs)) - if self.outputs != other.outputs: + % (self.ninputs, other.ninputs)) + if self.noutputs != other.noutputs: raise ValueError( "The first summand has %i output(s), but the second has %i." - % (self.outputs, other.outputs)) + % (self.noutputs, other.noutputs)) dt = common_timebase(self.dt, other.dt) # Preallocate the numerator and denominator of the sum. - num = [[[] for j in range(self.inputs)] for i in range(self.outputs)] - den = [[[] for j in range(self.inputs)] for i in range(self.outputs)] + num = [[[] for j in range(self.ninputs)] for i in range(self.noutputs)] + den = [[[] for j in range(self.ninputs)] for i in range(self.noutputs)] - for i in range(self.outputs): - for j in range(self.inputs): + for i in range(self.noutputs): + for j in range(self.ninputs): num[i][j], den[i][j] = _add_siso( self.num[i][j], self.den[i][j], other.num[i][j], other.den[i][j]) @@ -492,19 +492,19 @@ def __mul__(self, other): """Multiply two LTI objects (serial connection).""" # Convert the second argument to a transfer function. if isinstance(other, (int, float, complex, np.number)): - other = _convert_to_transfer_function(other, inputs=self.inputs, - outputs=self.inputs) + other = _convert_to_transfer_function(other, inputs=self.ninputs, + outputs=self.ninputs) else: other = _convert_to_transfer_function(other) # Check that the input-output sizes are consistent. - if self.inputs != other.outputs: + if self.ninputs != other.noutputs: raise ValueError( "C = A * B: A has %i column(s) (input(s)), but B has %i " - "row(s)\n(output(s))." % (self.inputs, other.outputs)) + "row(s)\n(output(s))." % (self.ninputs, other.noutputs)) - inputs = other.inputs - outputs = self.outputs + inputs = other.ninputs + outputs = self.noutputs dt = common_timebase(self.dt, other.dt) @@ -514,13 +514,13 @@ def __mul__(self, other): # Temporary storage for the summands needed to find the (i, j)th # element of the product. - num_summand = [[] for k in range(self.inputs)] - den_summand = [[] for k in range(self.inputs)] + num_summand = [[] for k in range(self.ninputs)] + den_summand = [[] for k in range(self.ninputs)] # Multiply & add. for row in range(outputs): for col in range(inputs): - for k in range(self.inputs): + for k in range(self.ninputs): num_summand[k] = polymul( self.num[row][k], other.num[k][col]) den_summand[k] = polymul( @@ -536,19 +536,19 @@ def __rmul__(self, other): # Convert the second argument to a transfer function. if isinstance(other, (int, float, complex, np.number)): - other = _convert_to_transfer_function(other, inputs=self.inputs, - outputs=self.inputs) + other = _convert_to_transfer_function(other, inputs=self.ninputs, + outputs=self.ninputs) else: other = _convert_to_transfer_function(other) # Check that the input-output sizes are consistent. - if other.inputs != self.outputs: + if other.ninputs != self.noutputs: raise ValueError( "C = A * B: A has %i column(s) (input(s)), but B has %i " - "row(s)\n(output(s))." % (other.inputs, self.outputs)) + "row(s)\n(output(s))." % (other.ninputs, self.noutputs)) - inputs = self.inputs - outputs = other.outputs + inputs = self.ninputs + outputs = other.noutputs dt = common_timebase(self.dt, other.dt) @@ -559,12 +559,12 @@ def __rmul__(self, other): # Temporary storage for the summands needed to find the # (i, j)th element # of the product. - num_summand = [[] for k in range(other.inputs)] - den_summand = [[] for k in range(other.inputs)] + num_summand = [[] for k in range(other.ninputs)] + den_summand = [[] for k in range(other.ninputs)] for i in range(outputs): # Iterate through rows of product. for j in range(inputs): # Iterate through columns of product. - for k in range(other.inputs): # Multiply & add. + for k in range(other.ninputs): # Multiply & add. num_summand[k] = polymul(other.num[i][k], self.num[k][j]) den_summand[k] = polymul(other.den[i][k], self.den[k][j]) num[i][j], den[i][j] = _add_siso( @@ -579,13 +579,13 @@ def __truediv__(self, other): if isinstance(other, (int, float, complex, np.number)): other = _convert_to_transfer_function( - other, inputs=self.inputs, - outputs=self.inputs) + other, inputs=self.ninputs, + outputs=self.ninputs) else: other = _convert_to_transfer_function(other) - if (self.inputs > 1 or self.outputs > 1 or - other.inputs > 1 or other.outputs > 1): + if (self.ninputs > 1 or self.noutputs > 1 or + other.ninputs > 1 or other.noutputs > 1): raise NotImplementedError( "TransferFunction.__truediv__ is currently \ implemented only for SISO systems.") @@ -606,13 +606,13 @@ def __rtruediv__(self, other): """Right divide two LTI objects.""" if isinstance(other, (int, float, complex, np.number)): other = _convert_to_transfer_function( - other, inputs=self.inputs, - outputs=self.inputs) + other, inputs=self.ninputs, + outputs=self.ninputs) else: other = _convert_to_transfer_function(other) - if (self.inputs > 1 or self.outputs > 1 or - other.inputs > 1 or other.outputs > 1): + if (self.ninputs > 1 or self.noutputs > 1 or + other.ninputs > 1 or other.noutputs > 1): raise NotImplementedError( "TransferFunction.__rtruediv__ is currently implemented only " "for SISO systems.") @@ -697,7 +697,7 @@ def pole(self): def zero(self): """Compute the zeros of a transfer function.""" - if self.inputs > 1 or self.outputs > 1: + if self.ninputs > 1 or self.noutputs > 1: raise NotImplementedError( "TransferFunction.zero is currently only implemented " "for SISO systems.") @@ -709,8 +709,8 @@ def feedback(self, other=1, sign=-1): """Feedback interconnection between two LTI objects.""" other = _convert_to_transfer_function(other) - if (self.inputs > 1 or self.outputs > 1 or - other.inputs > 1 or other.outputs > 1): + if (self.ninputs > 1 or self.noutputs > 1 or + other.ninputs > 1 or other.noutputs > 1): # TODO: MIMO feedback raise NotImplementedError( "TransferFunction.feedback is currently only implemented " @@ -741,11 +741,11 @@ def minreal(self, tol=None): sqrt_eps = sqrt(float_info.epsilon) # pre-allocate arrays - num = [[[] for j in range(self.inputs)] for i in range(self.outputs)] - den = [[[] for j in range(self.inputs)] for i in range(self.outputs)] + num = [[[] for j in range(self.ninputs)] for i in range(self.noutputs)] + den = [[[] for j in range(self.ninputs)] for i in range(self.noutputs)] - for i in range(self.outputs): - for j in range(self.inputs): + for i in range(self.noutputs): + for j in range(self.ninputs): # split up in zeros, poles and gain newzeros = [] @@ -811,10 +811,10 @@ def returnScipySignalLTI(self, strict=True): kwdt = {} # Preallocate the output. - out = [[[] for j in range(self.inputs)] for i in range(self.outputs)] + out = [[[] for j in range(self.ninputs)] for i in range(self.noutputs)] - for i in range(self.outputs): - for j in range(self.inputs): + for i in range(self.noutputs): + for j in range(self.ninputs): out[i][j] = signalTransferFunction(self.num[i][j], self.den[i][j], **kwdt) @@ -843,7 +843,7 @@ def _common_den(self, imag_tol=None, allow_nonproper=False): Returns ------- num: array - n by n by kd where n = max(sys.outputs,sys.inputs) + n by n by kd where n = max(sys.noutputs,sys.ninputs) kd = max(denorder)+1 Multi-dimensional array of numerator coefficients. num[i,j] gives the numerator coefficient array for the ith output and jth @@ -853,7 +853,7 @@ def _common_den(self, imag_tol=None, allow_nonproper=False): order of the common denominator, num will be returned as None den: array - sys.inputs by kd + sys.ninputs by kd Multi-dimensional array of coefficients for common denominator polynomial, one row per input. The array is prepared for use in slycot td04ad, the first element is the highest-order polynomial @@ -872,7 +872,7 @@ def _common_den(self, imag_tol=None, allow_nonproper=False): # Machine precision for floats. eps = finfo(float).eps - real_tol = sqrt(eps * self.inputs * self.outputs) + real_tol = sqrt(eps * self.ninputs * self.noutputs) # The tolerance to use in deciding if a pole is complex if (imag_tol is None): @@ -880,7 +880,7 @@ def _common_den(self, imag_tol=None, allow_nonproper=False): # A list to keep track of cumulative poles found as we scan # self.den[..][..] - poles = [[] for j in range(self.inputs)] + poles = [[] for j in range(self.ninputs)] # RvP, new implementation 180526, issue #194 # BG, modification, issue #343, PR #354 @@ -891,9 +891,9 @@ def _common_den(self, imag_tol=None, allow_nonproper=False): # do not calculate minreal. Rory's hint .minreal() poleset = [] - for i in range(self.outputs): + for i in range(self.noutputs): poleset.append([]) - for j in range(self.inputs): + for j in range(self.ninputs): if abs(self.num[i][j]).max() <= eps: poleset[-1].append([array([], dtype=float), roots(self.den[i][j]), 0.0, [], 0]) @@ -902,8 +902,8 @@ def _common_den(self, imag_tol=None, allow_nonproper=False): poleset[-1].append([z, p, k, [], 0]) # collect all individual poles - for j in range(self.inputs): - for i in range(self.outputs): + for j in range(self.ninputs): + for i in range(self.noutputs): currentpoles = poleset[i][j][1] nothave = ones(currentpoles.shape, dtype=bool) for ip, p in enumerate(poles[j]): @@ -928,20 +928,20 @@ def _common_den(self, imag_tol=None, allow_nonproper=False): # figure out maximum number of poles, for sizing the den maxindex = max([len(p) for p in poles]) - den = zeros((self.inputs, maxindex + 1), dtype=float) - num = zeros((max(1, self.outputs, self.inputs), - max(1, self.outputs, self.inputs), + den = zeros((self.ninputs, maxindex + 1), dtype=float) + num = zeros((max(1, self.noutputs, self.ninputs), + max(1, self.noutputs, self.ninputs), maxindex + 1), dtype=float) - denorder = zeros((self.inputs,), dtype=int) + denorder = zeros((self.ninputs,), dtype=int) havenonproper = False - for j in range(self.inputs): + for j in range(self.ninputs): if not len(poles[j]): # no poles matching this input; only one or more gains den[j, 0] = 1.0 - for i in range(self.outputs): + for i in range(self.noutputs): num[i, j, 0] = poleset[i][j][2] else: # create the denominator matching this input @@ -951,7 +951,7 @@ def _common_den(self, imag_tol=None, allow_nonproper=False): denorder[j] = maxindex # now create the numerator, also padded on the right - for i in range(self.outputs): + for i in range(self.noutputs): # start with the current set of zeros for this output nwzeros = list(poleset[i][j][0]) # add all poles not found in the original denominator, @@ -1068,9 +1068,9 @@ def _dcgain_cont(self): """_dcgain_cont() -> DC gain as matrix or scalar Special cased evaluation at 0 for continuous-time systems.""" - gain = np.empty((self.outputs, self.inputs), dtype=float) - for i in range(self.outputs): - for j in range(self.inputs): + gain = np.empty((self.noutputs, self.ninputs), dtype=float) + for i in range(self.noutputs): + for j in range(self.ninputs): num = self.num[i][j][-1] den = self.den[i][j][-1] if den: @@ -1228,14 +1228,14 @@ def _convert_to_transfer_function(sys, **kw): return sys elif isinstance(sys, StateSpace): - if 0 == sys.states: + if 0 == sys.nstates: # Slycot doesn't like static SS->TF conversion, so handle # it first. Can't join this with the no-Slycot branch, # since that doesn't handle general MIMO systems - num = [[[sys.D[i, j]] for j in range(sys.inputs)] - for i in range(sys.outputs)] - den = [[[1.] for j in range(sys.inputs)] - for i in range(sys.outputs)] + num = [[[sys.D[i, j]] for j in range(sys.ninputs)] + for i in range(sys.noutputs)] + den = [[[1.] for j in range(sys.ninputs)] + for i in range(sys.noutputs)] else: try: from slycot import tb04ad @@ -1247,17 +1247,17 @@ def _convert_to_transfer_function(sys, **kw): # Use Slycot to make the transformation # Make sure to convert system matrices to numpy arrays tfout = tb04ad( - sys.states, sys.inputs, sys.outputs, array(sys.A), + sys.nstates, sys.ninputs, sys.noutputs, array(sys.A), array(sys.B), array(sys.C), array(sys.D), tol1=0.0) # Preallocate outputs. - num = [[[] for j in range(sys.inputs)] - for i in range(sys.outputs)] - den = [[[] for j in range(sys.inputs)] - for i in range(sys.outputs)] + num = [[[] for j in range(sys.ninputs)] + for i in range(sys.noutputs)] + den = [[[] for j in range(sys.ninputs)] + for i in range(sys.noutputs)] - for i in range(sys.outputs): - for j in range(sys.inputs): + for i in range(sys.noutputs): + for j in range(sys.ninputs): num[i][j] = list(tfout[6][i, j, :]) # Each transfer function matrix row # has a common denominator. @@ -1265,7 +1265,7 @@ def _convert_to_transfer_function(sys, **kw): except ImportError: # If slycot is not available, use signal.lti (SISO only) - if sys.inputs != 1 or sys.outputs != 1: + if sys.ninputs != 1 or sys.noutputs != 1: raise TypeError("No support for MIMO without slycot.") # Do the conversion using sp.signal.ss2tf From 9d469f196eccda759a99e175729080c8ecf912f3 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 20 Jan 2021 22:33:11 -0800 Subject: [PATCH 2/4] update getter/setter comments to match code --- control/lti.py | 6 +++--- control/statesp.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/control/lti.py b/control/lti.py index 04f495838..97a1e9b7f 100644 --- a/control/lti.py +++ b/control/lti.py @@ -54,9 +54,9 @@ def __init__(self, inputs=1, outputs=1, dt=None): # # Getter and setter functions for legacy input/output attributes # - # For this iteration, generate a warning whenever the getter/setter is - # called. For a future iteration, turn it iinto a pending deprecation and - # then deprecation warning (commented out for now). + # For this iteration, generate a pending deprecation warning whenever + # the getter/setter is called. For a future iteration, turn it into a + # deprecation warning. # @property diff --git a/control/statesp.py b/control/statesp.py index a6851e531..8a47f3e74 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -333,9 +333,9 @@ def __init__(self, *args, **kwargs): # # Getter and setter functions for legacy state attributes # - # For this iteration, generate a warning whenever the getter/setter is - # called. For a future iteration, turn it iinto a pending deprecation and - # then deprecation warning (commented out for now). + # For this iteration, generate a pending deprecation warning whenever + # the getter/setter is called. For a future iteration, turn it into a + # deprecation warning. # @property From f633874054a9811412633cf4647fc3a1f449d2c8 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 22 Jan 2021 08:17:36 -0800 Subject: [PATCH 3/4] add unit test for input/output/state deprecation warning --- control/tests/lti_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index b4a168841..5c5b65e4a 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -250,3 +250,14 @@ def test_squeeze_exceptions(self, fcn): sys.frequency_response([[0.1, 1], [1, 10]]) sys([[0.1, 1], [1, 10]]) evalfr(sys, [[0.1, 1], [1, 10]]) + + with pytest.raises(PendingDeprecationWarning, match="LTI `inputs`"): + assert sys.inputs == sys.ninputs + + with pytest.raises(PendingDeprecationWarning, match="LTI `outputs`"): + assert sys.outputs == sys.noutputs + + if isinstance(sys, ct.StateSpace): + with pytest.raises( + PendingDeprecationWarning, match="StateSpace `states`"): + assert sys.states == sys.nstates From eb401a94e426dc3cc9c30108fa8841682144a6c0 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 24 Jan 2021 08:38:51 -0800 Subject: [PATCH 4/4] fix up deprecation warnings in response to @bnavigator review --- control/lti.py | 33 ++++++++++++++++----------------- control/statesp.py | 19 +++++++++---------- control/tests/lti_test.py | 17 ++++++++++------- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/control/lti.py b/control/lti.py index 97a1e9b7f..445775f5f 100644 --- a/control/lti.py +++ b/control/lti.py @@ -52,40 +52,39 @@ def __init__(self, inputs=1, outputs=1, dt=None): self.dt = dt # - # Getter and setter functions for legacy input/output attributes + # Getter and setter functions for legacy state attributes # - # For this iteration, generate a pending deprecation warning whenever - # the getter/setter is called. For a future iteration, turn it into a - # deprecation warning. + # For this iteration, generate a deprecation warning whenever the + # getter/setter is called. For a future iteration, turn it into a + # future warning, so that users will see it. # @property def inputs(self): - raise PendingDeprecationWarning( - "The LTI `inputs` attribute will be deprecated in a future " - "release. Use `ninputs` instead.") + warn("The LTI `inputs` attribute will be deprecated in a future " + "release. Use `ninputs` instead.", + DeprecationWarning, stacklevel=2) return self.ninputs @inputs.setter def inputs(self, value): - raise PendingDeprecationWarning( - "The LTI `inputs` attribute will be deprecated in a future " - "release. Use `ninputs` instead.") - + warn("The LTI `inputs` attribute will be deprecated in a future " + "release. Use `ninputs` instead.", + DeprecationWarning, stacklevel=2) self.ninputs = value @property def outputs(self): - raise PendingDeprecationWarning( - "The LTI `outputs` attribute will be deprecated in a future " - "release. Use `noutputs` instead.") + warn("The LTI `outputs` attribute will be deprecated in a future " + "release. Use `noutputs` instead.", + DeprecationWarning, stacklevel=2) return self.noutputs @outputs.setter def outputs(self, value): - raise PendingDeprecationWarning( - "The LTI `outputs` attribute will be deprecated in a future " - "release. Use `noutputs` instead.") + warn("The LTI `outputs` attribute will be deprecated in a future " + "release. Use `noutputs` instead.", + DeprecationWarning, stacklevel=2) self.noutputs = value def isdtime(self, strict=False): diff --git a/control/statesp.py b/control/statesp.py index 8a47f3e74..47018b497 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -333,24 +333,23 @@ def __init__(self, *args, **kwargs): # # Getter and setter functions for legacy state attributes # - # For this iteration, generate a pending deprecation warning whenever - # the getter/setter is called. For a future iteration, turn it into a - # deprecation warning. + # For this iteration, generate a deprecation warning whenever the + # getter/setter is called. For a future iteration, turn it into a + # future warning, so that users will see it. # @property def states(self): - raise PendingDeprecationWarning( - "The StateSpace `states` attribute will be deprecated in a future " - "release. Use `nstates` instead.") + warn("The StateSpace `states` attribute will be deprecated in a " + "future release. Use `nstates` instead.", + DeprecationWarning, stacklevel=2) return self.nstates @states.setter def states(self, value): - raise PendingDeprecationWarning( - "The StateSpace `states` attribute will be deprecated in a future " - "release. Use `nstates` instead.") - # raise PendingDeprecationWarning( + warn("The StateSpace `states` attribute will be deprecated in a " + "future release. Use `nstates` instead.", + DeprecationWarning, stacklevel=2) self.nstates = value def _remove_useless_states(self): diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index 5c5b65e4a..1bf633e84 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -251,13 +251,16 @@ def test_squeeze_exceptions(self, fcn): sys([[0.1, 1], [1, 10]]) evalfr(sys, [[0.1, 1], [1, 10]]) - with pytest.raises(PendingDeprecationWarning, match="LTI `inputs`"): - assert sys.inputs == sys.ninputs + with pytest.warns(DeprecationWarning, match="LTI `inputs`"): + ninputs = sys.inputs + assert ninputs == sys.ninputs - with pytest.raises(PendingDeprecationWarning, match="LTI `outputs`"): - assert sys.outputs == sys.noutputs + with pytest.warns(DeprecationWarning, match="LTI `outputs`"): + noutputs = sys.outputs + assert noutputs == sys.noutputs if isinstance(sys, ct.StateSpace): - with pytest.raises( - PendingDeprecationWarning, match="StateSpace `states`"): - assert sys.states == sys.nstates + with pytest.warns( + DeprecationWarning, match="StateSpace `states`"): + nstates = sys.states + assert nstates == sys.nstates