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 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