Skip to content

Commit

Permalink
Merge pull request #515 from murrayrm/input_output_to_ninput_noutput
Browse files Browse the repository at this point in the history
Switch LTI class and subclasses to use ninputs, noutputs, nstates
  • Loading branch information
bnavigator committed Jan 24, 2021
2 parents 1502d38 + eb401a9 commit eb146a6
Show file tree
Hide file tree
Showing 21 changed files with 457 additions and 385 deletions.
16 changes: 8 additions & 8 deletions control/bdalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:]:
Expand All @@ -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):
Expand Down
14 changes: 7 additions & 7 deletions control/canonical.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,24 +79,24 @@ 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
Tzx = solve(Wrx.T, Wrz.T).T # matrix right division, Tzx = Wrz * inv(Wrx)

# 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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
54 changes: 27 additions & 27 deletions control/frdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)):
Expand All @@ -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)
Expand All @@ -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.")
Expand All @@ -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.")
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand Down
6 changes: 3 additions & 3 deletions control/freqplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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.")
Expand Down
22 changes: 11 additions & 11 deletions control/iosys.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,25 +659,25 @@ 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)

# 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):
Expand Down Expand Up @@ -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)
Expand Down
48 changes: 42 additions & 6 deletions control/lti.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,46 @@ 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 state attributes
#
# 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):
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):
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):
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):
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):
"""
Check to see if a system is a discrete-time system
Expand Down Expand Up @@ -88,7 +124,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
Expand Down Expand Up @@ -126,7 +162,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
Expand Down Expand Up @@ -475,7 +511,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
Expand Down Expand Up @@ -532,7 +568,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
Expand Down

0 comments on commit eb146a6

Please sign in to comment.